In this tutorial, you will develop a Reverse Polish Notation (RPN) calculator. According to Wikipedia, PRN was developed by 1920 by Polish mathematician Jan Lukasiewicz, so the notation has some history. The purpose of this tutorial is to give you practice with BDD on an object with methods, rather than an object with one method, as in the first tutorial.
As with the first tutorial, this tutorial covers:
The three laws of TDD
Continuous refactoring
Basic continuous integration
Unlike the first tutorial, this tutorial will:
Spend more time discussing a few more of the S.O.L.I.D. principles,
Refactoring to patterns
Opening up classes using Ruby's Meta-Object Protocol
By the end of this tutorial, you'll be ready to start using story tests on a more complicated problem.
Note
This tutorial assumes you've either already worked through the first tutorial or are familiar with BDD and RSpec. If not, here are a few terms that come up early enough you'll need to know them:
Example - in RSpec parlance, this refers to a series of steps describing a use of the system. Before writing production code, you'll write an example. That example will not run most of the time, so you'll then create the production code necessary to get the example to pass.
Context - A context forms a common environment under-which Examples execute. A context may just be a name or it might include common setup and initialization as well as common clean-up. Contexts can be nested, which was demonstrated in the first tutorial.
Approach
Unlike the first tutorial, this tutorial will be a bit more explicit in its steps. Where reasonable, each Example will follow the same flow:
Title: Proceeded by a solid line, this will be at Heading Level 1
Overview: The overview immediately follows the title
Example: The Example be in a fixed-point font with different background.
Check-in: As with the other tutorials, getting you to practice frequent checkings is a secondary goal so there will be not-so-subtle reminders along the way. If you find your tools don't support such a working style, that's valuable information as well.
Refactor: There's almost always room for improvement, so the tutorial will review the work it just had you do and make suggestions as applicable. These refactorings are generally of the seconds-to-minutes variety. If an example requires a more significant restructuring, then it will be an entire sub-section in the Example.
Check-in: After refactoring, checking in again.
Summary: A synopsis of anything new or significant
There will also be occasional side-bars on an as-needed basis.
Sidebar: The Three Laws of TDD Revisited
Here are Robert Martin's three laws of TDD written from a BDD perspective:
Write no production code without a failing Example
Write only enough of an Example sufficient for it to fail (not compiling is failing)
Write only enough production code to get the single failing Example to pass
These rules are not enough to effectively write code using either BDD or TDD. Here are a few more things to take into consideration:
Keep your code clean: as you notice unruly code, tame it. However, do this only when your Examples are all passing
Keep existing Examples passing
When you are looking at code, apply a critical eye and look for violations of sound design principles. (What are those principles? The tutorial will describe violations as they become ugly enough to warrant taking your time to discuss them.)
Before you get started on any Examples, however, we'll delve into the problem just a bit.
The Problem
An RPN calculator works by performing calculations on operands that the user has already entered. Here is an example scenario:
5 <enter>
3
+
= 8
In this example, a user enters first 5 and then 3 and hits the + key to get the previous two operands 5 and 3, added together.
In general, using an RPN calculator obviates the need to use parenthesis. Consider the following:
That same expression could be calculated on an RPN calculator as follows:
5 <enter> 3 + 6 * 3 / 4 ^
=65536
An RPN calculator as a stack of previously entered values. The most recently entered numbers are the numbers that become available first (Last In First Out - LIFO). Consider this longer sequence:
3 <enter> 6 <enter> -8 <enter> 89 + sqrt - +
=0
Here's the evaluation of the above expression:
3, 6, -8 get places on the operand stack
8 is placed in "working storage" or in HP terminology, the x register
+, a binary operator, takes as its left hand operand the top item on the stack, -8 and as its right hand operand the x register, 89. The result, 81, is placed in the x register
Next, sqrt, a unary operator, takes the value on the x register and calculated 9, which is then put into the x register
The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register
The first -, another binary operator, takes as its left-hand operator the top value on the stack, which is currently 6. It takes as its right-hand operator the x register, which is 9. The result, -3, is placed in the x register.
The final operator, +, another binary operator, takes as its first operand the top item on the stack, which is 3. For its right-hand operand, it takes the x register, -3. Adding, the result is 0, which becomes the value in the x register.
The stack is empty.
The x register
The x register behaves like an accumulator. As you type numbers, those numbers get added to what is already there. When you type a non-number key, it applies itself. For example, the <enter> key really behaves like a unary operator. When you press <enter>, it takes the value in the x register and places it on the stack.
If the next key is part of a number, the x register is reset and the key pressed becomes the contents of the x register.
If the next key is not part of a number, then it applies as is.
After any non-number key, the x register is "locked" and any attempt to change it results in it being reset.
Getting Started
There are several major parts to this problem:
Handling input
Handling the stack and the x register properly
Processing the basic operators (+, -, /, *, ^)
Handling more complex functions (e.g., prime factors, sum over the stack)
In addition, we'll add several things to make this a bit more interesting:
CRUD'ing (Create, Read, Update, Delete) variables
CRUD'ing user-defined functions
Behavior Driven Development advocates starting outside in; start with the user experience and then work your way into the system. In this tutorial, you will not strictly be creating a user interface; you will, however, create the stuff necessary to build a user interface. The tutorial will start at the individual entry of keys and work up to basic math.
Your Work Area
Create a directory to store all of your work. Unlike the first tutorial, you'll be working in multiple files this time.
The 0th Example
As with all problems, you need to pick a starting place. As with the first tutorial, you will create a basic example describing object creation. Next, you'll continue with a series of examples centered on basic input. Thinking further ahead at this point might constitute analysis paralysis.
Example
Here is a first Example for this problem:
describe "Basic Creation"do
it "should be non-null after creation"do
calculator = RpnCalculator.newendend
Create this Example in your directory, call the file "rpn_calculator_spec.rb"
Execute the Example, it fails:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
Basic Creation
- should be non-null after creation (ERROR - 1)
1)
NameError in 'Basic Creation should be non-null after creation'
uninitialized constant RpnCalculator
./rpn_calculator_spec.rb:3:
Finished in 0.013091 seconds
1 example, 1 failure
The -c option adds color to the output. You won't see the color in this tutorial. Also, if you are running this in a DOS terminal window, then you'll want to leave this option off.
The message describes an unknown constant, RpnCalcualtor. You'll need to create that class.
Create the following Ruby class in a file called rpn_calculator.rb:
class RpnCalculator
end
Update rpn_calculator_spec.rb to require the file you just created:
require'rpn_calculator'
describe "Basic Creation"do
...
end
Run your Example again, you should be all green:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
Basic Creation
- should be non-null after creation
Finished in 0.0123 seconds
1 example, 0 failures
OK, you have an Example but it does not contain any verification. So one more change and one more execution.
Update the Example to validate calculator:
calculator.should_not be nil
Run your Example again, you should be all green:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
Basic Creation
- should be non-null after creation
Finished in0.0123 seconds
1 example, 0 failures
Sidebar: Examples passing without validation
Generally, we want Examples to fail for an expected reason before they pass. This case is an exception to that rule, but you'll see that coming up.
Sidebar: All Green
All green refers to the state of the executing Example; it means all Examples are executing and passing. This terminology derives from unit testing in Smalltalk and then popularized by JUnit. In Smalltalk, their process was:
Red: Create a failing test
Green: Get the failing test to pass
Blue: Refactor
This is essentially what this tutorial advocates. JUnit turned this into red-bar, green-bar. When the GUI of JUnit executes, it has a progress bar that indicates either:
Red: one more more tests have failed
Green: all tests executed so far have passed
Check-In
You are all green, so it is time to check in your work. As with the previous tutorial, I'll be using git:
Note that in this example I have a file you will not have, wikiwriteup.txt. That's the raw material used for format the wikipage you are now reading.
Refactor
A quick review of the existing Example and production code does not suggest any refactoring yet.
Check-In
Since there was no need to refactor, there's no need to check in again.
Summary
You created two files: rpn_calculator_spec.rb
require'rpn_calculator'
describe "Basic Creation"do
it "should be non-null after creation"do
calculator = RpnCalculator.new
calculator.should_not be nilendend
rpn_calculator
class RpnCalculator
end
This may seem like a trivial start, and that is by design. What have you verified so far:
You have a working environment
The RSpec gem is installed
Ruby is intalled
You have the basic files in place
You've checked your work into a repository
Not every Example will be so small and quick to create, but you should attempt to break your work down into bite-sized chunks like this. Why?
If you get distracted, you don't lose very much ground.
If you mess up and need a do-over, you don't lose too much work.
If someone needs your attention, you're not too far from having a great place to stop what you're doing.
You can feel a sense of accomplishment throughout the day.
You can watch as your system organically grows from something trivial, to something complex, typically quickly.
Sidebar: A Concrete Example of a Hardware Simulator
In mid 2008, I worked with a great group of people on the East Coast. My task was to use their current problem as source material and to take them through TDD (not BDD) on their problem.
They already had a start on the problem and had pretty good source material on what the system needed to do. The system essentially simulated expensive hardware. This system would make testing the software to drive the expensive hardware. The problem involved capturing an over-the-wire protocol, which they were already doing in C++. The problem, then, was to take that protocol and respond appropriately and even initiate messages.
During the week I was there, we:
Created an initially simple solution that evolved into something that nearly simulated a complete, albeit simple, "job".
We had fully tested the execution of the protocol, including starting the system up in simulated mode (staring a Java virtual Machine [JVM] directly) versus real mode, starting a JVM from a C++ application.
On Tuesday, the week after I left, they had managed to fix a few problems on protocol capture and had successfully processed a job. On Wednesday, they were successfully processing a number of simple jobs.
A few weeks later they were processing complex jobs and then a few weeks later they were processing up to 15 simultaneous complex jobs. Their simulator was fast enough that they had to slow it down.
In a very real sense, starting with a granule and letting the system grow through accretion is a viable way to build complex systems.
Example: Accepting User Input
With the first Example running it is time to move on to basic user input. We have to make a decision already, where is the system boundary. Here are a few possibilities:
The system receives a series of events for buttons pressed, e.g., pressing "9" on the UI generates a 9-button pressed event, pressing "sqrt" on the UI generates a sqrt-button pressed event.
The system receives "digit" messages for digits or "function" messages for functions.
The system receives messages such as a complete number, enter, + and so on.
There are other alternatives, and to some extent, the selection is arbitrary. There is no decision on the UI technology there's no way to know the eventing mechanism, if applicable. Even so, you can make progress based on some logical representation and then adapt between the real UI technology and the system you've developed.
A standard model for handling user input is the Model:View:Control pattern. In this case, the View is the as-yet-to-be-determined presentation technology. The Model is the calculator itself. The control part of the equation maps between the view and the model (the UI and the calculator). So you will create the model, drive the system using RSpec as the de-facto control, and when you've selected the UI technology, you'll create a new control to map from the UI to your well-tested model.
As a side benefit, most of the logic will be in easy to test code.
Since the particulars of the UI are irrelevant, this tutorial will move forward using option 2 listed above for handling numbers. When it is time to handle things like +, /, etc., it'll be time to make another decision.
Example
Here is the beginning of an example:
describe "Handling User Input of Numbers"do
it "should store a series of digits in the x register"do
calculator = RpnCalculator.new
calculator.digit_pressed'4'endend
There's more to be written, but as soon as the message "digit_pressed" is sent to a calculator, the Example will fail.
Create this Example, adding it to rpn_calculator_spec.rb.
Execute the Example, you should see an error similar to the following:
1)
NoMethodError in 'Handling User Input of Numbers should store a series of digits in the x register'
undefined method `digit_pressed' for #<RpnCalculator:0x58kkc80c>
./rpn_calculator_spec.rb:12:
Finished in 0.015707 seconds
2 examples, 1 failure
Assuming you worked through the first BDD tutorial, this is familiar ground. You need to add a method to get this to compiled (you've just finished applying law 2 of the 3 laws of BDD).
Update RpnCalculator:
class RpnCalculator
def digit_pressed(digit)endend
Verify that your Example now executes successfully.
Complete the Example:
describe "Handling User Input of Numbers"do
it "should store a series of digits in the x register"do
calculator = RpnCalculator.new
calculator.digit_pressed'4'
calculator.digit_pressed'2'
calculator.x_register.should == 42endend
Execute your examples, one will fail:
1)
NoMethodError in 'Handling User Input of Numbers should store a series of digits in the x register'
undefined method `x_register' for #<RpnCalculator:0x58c5dc>
./rpn_calculator_spec.rb:14:
Finished in 0.015999 seconds
2 examples, 1 failure
As with the previous failure, this failure is a result of a missing method.
First, create the method in RpnCalculator to get your code to compiling:
def x_register
end
Run your Example, make sure it is failing:
1)
'Handling User Input of Numbers should store a series of digits in the x register' FAILED
expected: 42,
got: nil (using ==)
./rpn_calculator_spec.rb:14:
Finished in 0.016444 seconds
2 examples, 1 failure
Now, do the simplest thing that could work, AKA fast-green bar, AKA, Uncle Bob's TDD rules, #3:
def x_register
42end
Finally, run your Examples to make sure they are passing:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
Basic Creation
- should be non-null after creation
Handling User Input of Numbers
- should store a series of digits in the x register
Finished in 0.014956 seconds
2 examples, 0 failures
Sidebar: Why two steps instead of just one?
Why not just create a method and in the same step get your Example passing? In this trivial example you could and it would not be too risky. However, in general you should make sure your Examples fail for the right reason before getting them to pass. Why? It serves as a check that the Example you've written is doing what you think it is doing.
With proper IDE support, the intermediate failing step to the working step is seconds. Even with the environment I'm using, two terminal windows, the work is 10 - 15 seconds. So while this is "extra" work, it has a purpose.
Eventually you will see these tutorials skipping steps. But for now, the tutorial is keeping the rigor high.
Sidebar: What a silly implementation!
What about the hard-coded return value? Why not just write the code now, it's clear what needs to be done. This is an issue of balancing what you know and the risk that what you know is incorrect.
In this case, you could probably guess where this is going. However, as I write the tutorial, this is literally the first time I've worked on this part of this problem, so while I think I know what's going to happen, I'm really not sure. And even if I know the next step, I am not too sure about 2, 3 or more steps ahead.
A general answer to this complaint is that rather than writing what you know the solution should be, write an Example that will force/allow you to write the code you know should be there.
The primary danger of jumping ahead is writing too much production code. That code may not be well covered by your Examples, so you'll essentially have unvalidated code. In addition, you could write more than is necessary. If you do that, the next person to read the code might wonder why there's more code than the necessary. You might be that next person, by the way.
Check-In
Check in your work:
Macintosh-7% git commit -a
Created commit 727745f: Added basic support for handling digits.
3 files changed, 215 insertions(+), 2 deletions(-)
Refactor
Review of the RpnCalculator class does not suggest any refactoring.
Review of the Examples, however, does. There is some minor duplication. Both Examples create a calculator. This is a quick fix since you can create a containing Context with common setup. As with any refactoring, however, you'll take small steps and verify as you go along.
Create a containing context: Above the first describe
describe "RPN Calculator"do
After the last end
end
Update the spacing - that's one command in VI, the best-ever editor
describe "RPN Calculator"
describe "Basic Creation"do
...
end
describe "Handling User Input of Numbers"do
...
endend
Verify that your Examples still execute:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling User Input of Numbers
- should store a series of digits in the x register
Finished in0.016923 seconds
2 examples, 0 failures
Add a common setup to your outer-context:
before(:each)do@calculator = RpnCalculator.newend
Verify your Examples still run.
Update the first Example to use the initialized calculator:
describe "Basic Creation"do
it "should be non-null after creation"do@calculator.should_not be nilendend
Verify your Examples still run.
Update the second Example as well:
describe "Handling User Input of Numbers"do
it "should store a series of digits in the x register"do@calculator.digit_pressed'4'@calculator.digit_pressed'2'@calculator.x_register.should == 42endend
Verify your Examples still run.
You might be tempted to do more at this point. For example, you know you'll be adding a lot of values to the x_register. However, let that generalization happen naturally. Practicing both BDD and TDD encourages bottom-up, example-based generalization.
Check-In
Check in your work:
Macintosh-7% git commit -a
Created commit b203185: Removed duplication of calculator initialization.
2 files changed, 128 insertions(+), 18 deletions(-)
rewrite rpn_calculator_spec.rb (90%)
Summary
The first example added basic object creation. This Example defined some of the API for entering digits. The production code doesn't actually process this yet, however, you already have executable documentation of how the API is supposed to work.
You also performed some basic refactoring on your Examples. While mentioned in the first tutorial, here are a few observations:
You want to get to the point where your muscle memory is imbued with this kind of basic cleanup. The only way to get there is practice.
We removed a violation of the DRY principle. I believe Ruby Dave Thomas is credited with saying all design is an exercise in removing duplication. Regardless of who said it, I think it's a sound idea.
Example: Taking a decimal point as well
The production code is hard-coded. Somehow you want to push your solution to remove that hard-coded value. One way to do that is to and another Example (don't do this):
describe "Handling an even longer User Input of Numbers"do
it "should store a series of digits in the x register"do@calculator.digit_pressed'1'@calculator.digit_pressed'2'@calculator.digit_pressed'4'@calculator.digit_pressed'3'@calculator.digit_pressed'2'@calculator.x_register.should == 12432end
On the one hand, this will force some more work in the production code. On the other hand, this Example is a proper superset of the first Example. That is, this Example completely covers the first Example, so, pragmatically, you should remove the first Example. More Examples does not necessarily mean better Examples. Each Example should push the solution a little further and avoid duplicating the work of another Example. Why?
More examples --> longer to run --> run less often --> less effective
When something breaks, you are likely to break more than one Example --> more maintenance --> Examples become a hassle to maintain --> Examples are commented out or left not passing --> nearly defeats the purpose of BDD.
Sidebar: Failing Tests: Not Idle Chit-Chat
This is not idle speculation, I've been on projects where this dynamic occurred. One example involved a situation where "certain tests" (this is TDD, not BDD) just failed and people tended to not worry about them. A developer added a new key-value pair to a hash table. This caused a test to fail. He figured that the test was failing for some other reason, so he ignored it.
In fact here's what was happening:
The hashtable was in an object being written as a blob
The new key-value pair pushed the object just over the limit of the blob size (4076 bytes -- 5000 bytes, limit was 4096)
So his change was breaking the test, but not always because sometimes the data was just small enough to fit.
If tests fail for the wrong reasons, it makes it easy for someone to assume that what they just did could not possibly break the test. Often that just means an assumption is about to be exposed for what it is.
The DRY principle also covers test coverage.
So what can you do? How about accepting a decimal point in the number?
Example
Here is another example that fits extends the functionality just a little but will also force the implementation of the production code:
it "should handle a string of digits with an embedded ."do@calculator.digit_pressed'1'@calculator.digit_pressed'3'@calculator.digit_pressed'.'@calculator.digit_pressed'3'@calculator.digit_pressed'1'@calculator.x_register.should == 13.31end
Create this Example.
Run your Examples, you should have one failure:
... <snip> ...
- should handle a string of digits with an embedded . (FAILED - 1)
1)
'RPN Calculator Handling User Input of Numbers should handle a string of digits with an embedded .' FAILED
expected: 13.31,
got: 42 (using ==)
./rpn_calculator_spec.rb:27:
Finished in 0.018738 seconds
3 examples, 1 failure
Now it is time to do something more than hard-code the response.
Add an initialization method to your RpnCalculator class:
def initialize
@input = ''end
Update the digit_pressed method to record the digit:
def digit_pressed(digit)@input<< digit.to_send
Update the x_register method to use the recorded input:
def x_register
@input.to_fend
Run your Examples, you should be all green:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling User Input of Numbers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
Finished in 0.018312 seconds
3 examples, 0 failures
Check-In
It is once again time to check in your code. Everything is green:
Macintosh-7% git commit -a
Created commit e10de87: Added support for numbers with decimals.
3 files changed, 135 insertions(+), 2 deletions(-)
And as a reminder, the reason there are so many insertions in this example is because my checkins include updates to the source file used to generate the page you are reading.
Refactor
Reviewing the RpnCalclator class, there's little to do right now. What about your Examples? There appears to be duplication in that there are several lines that call the digit_pressed method. Is this duplication? It does represent the underlying objects' API. Even so, how can you improve this?
Before making any improvements, you need a reason to make the improvement. It is to make the Example more readable, easier to write, easier to maintain, or some other characteristic?
How about allowing your Example to provide a number, which is then "typed" for you?
Add a method to the containing context:
describe "RPN Calculator"do
...
def enter_number(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }end
...
end
Run your Examples, they should still pass.
Update the "Handling User Input of Numbers" example:
it "should store a series of digits in the x register"do
enter_number 42@calculator.x_register.should == 42end
Run your Examples, they should still pass.
Update the "should handle a string of digits with an embedded ." Example:
it "should handle a string of digits with an embedded ."do
enter_number 13.31@calculator.x_register.should == 13.31end
Run your Examples, they should still pass.
Finally, the second Example does not start with should, which is a pretty well-followed practice. So fix it:
describe "Handling integers"do
it "should store a series of digits in the x register"do
enter_number 42@calculator.x_register.should == 42end
Run your Examples, they should still pass.
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling integers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
Finished in0.017654 seconds
3 examples, 0 failures
Check-In
Check in your changes:
Macintosh-7% git commit -a
Created commit a465d16: Removed duplication in writing examples taking numbes.
2 files changed, 56 insertions(+), 24 deletions(-)
Summary
Congratulations, you have basic support for processing numeric input. You managed to use an Example that pushed the design just a bit and ended up with code in each of the methods.
There is one strange thing, your code uses "digit_pressed" for the digits 0 through 9 and '.'. That's something worth considering.
Here is a snapshot of what your code should resemble at this point: rpn_calculator_spec.rb
require'rpn_calculator'
describe "RPN Calculator"do
before(:each)do@calculator = RpnCalculator.newenddef enter_number(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }end
describe "Basic Creation"do
it "should be non-null after creation"do@calculator.should_not be nilendend
describe "Handling integers"do
it "should store a series of digits in the x register"do
enter_number 42@calculator.x_register.should == 42end
it "should handle a string of digits with an embedded ."do
enter_number 13.31@calculator.x_register.should == 13.31endendend
What happens when a user presses the <enter> key on the calculator:
The current value in the x_register is placed on the stack of previous operands.
Subsequent, the next numeric digit causes the x_register to reset, but pressing <enter> again causes that value to placed on the stack again.
In fact, as you will see, pressing a non-numeric key generally "locks-down" the x_register and makes the next digit cause it to be reset. You can describe this we several Examples.
Example
Here is a Context with three as yet to be defined Examples:
describe "Handling the <enter> key"do
it "should copy x_register to top of stack"
it "should cause next digit to reset the x_register"
it "should allow the x_register to be <entered> again"end
Add this context as a sub-context of the RPN Calculator context.
Execute your Examples, you should notice that these three are listed as pending:
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack (PENDING: Not Yet Implemented)
- should cause next digit to reset the x_register (PENDING: Not Yet Implemented)
- should allow the x_register to be <entered> again (PENDING: Not Yet Implemented)
Pending:
RPN Calculator Handling the <enter> key should copy x_register to top of stack (Not Yet Implemented)
RPN Calculator Handling the <enter> key should cause next digit to reset the x_register (Not Yet Implemented)
RPN Calculator Handling the <enter> key should allow the x_register to be <entered> again (Not Yet Implemented)
Finished in 0.022477 seconds
6 examples, 0 failures, 3 pending
Before you can write any of these, you have to make a decision about how to indicate that the <enter> key was pressed. Here are a few options:
Add a method, enter_pressed.
Add a method, execute_function(:enter)
Is there a clear winner? There are trade-offs:
The first option suggests a "wide" API. That is, the number of methods on the PN Calculator grows as its functionality grows. This might sound "normal" but it could be a violation of the Open/Closed Principle.
The second option requires a symbol for each function.
The second option requires the RPN calculator to map from a symbol, :enter, to a behavior.
Sidebar: Open/Closed Principle
The Open/Closed Principle suggests that a class, once released into the wild, should not change again except for fixing errors. In Eiffel, this meant leaving a class alone and extending from it to add new behavior. In general, this can also mean depending on an abstraction (interface or abstract base class) and then adding subclasses to complete the behavior.
Why is this relevant? Gerald M. Weinberg describes a relationship between the size of a bug fix and the likelihood that the bug fix introduces new bugs. In his experience, the smaller the fix, the more likely a new bug will be introduced. In his research, 66% of all one-line bug fixes introduce new bugs.
So leaving working code alone is at the heart of the Open/Closed Principle.
Adding new methods to an existing class is in fact changing that class. Adding new methods to a class is less likely to cause problems, but it is not impossible break a class when you do add methods to it. This suggests adding methods to a class every time we add a new feature is probably a bad idea.
Another issue is, in general, the more methods on a class, the more likely it is that it will be hard to understand. Finally, having many methods on a class makes it more likely that the class will violate the Single Responsibility Principle.
This tutorial will take the second approach.
Add to the first Example (make sure to add the 'do' after the first line):
it "should copy x_register to top of stack"do
enter_number 654@calculator.execute_function(:enter)end
Why stop here? This Example is now failing because it invokes a method that does not yet exist.
Run your Examples to verify this:
1)
NoMethodError in 'RPN Calculator Handling the <enter> key should copy x_register to top of stack'
undefined method `execute_function' for #<RpnCalculator:0x587ac8 @input="654">
./rpn_calculator_spec.rb:33:
Finished in 0.023074 seconds
6 examples, 1 failure, 2 pending
Get the example back to Green by adding a method in RpnCalculator:
def execute_function(function_symbol)end
Continue with the Example:
it "should copy x_register to top of stack"do
enter_number 654@calculator.execute_function(:enter)@calculator.top.should == 654end
This both completes the Example and causes it not to work because the top method does not exist. You can verify this by running your Examples.
Add a top method:
def top
654end
Did you notice I just had you "cheat". You should have:
Created a method that would cause the Example to fail.
Run your Examples.
Updated the method to return 654
Run your Examples.
Did you notice that? If not, see how easy it is to slide backwards?
Sidebar: Just how much can you follow the three laws
If I have a proper IDE and decent refactoring tools, then I really do prefer following the three laws strictly. Unfortunately, I have not as of 2008/10 found an IDE that has two particular refactorings/corrections for Ruby:
Create class missing at the cursor location (preferably in its own file)
Create method missing at the cursor location
I have these in C++, C#, Java, VB.Net and others. Since I don't have these, I'd consider relaxing the second rule of TDD to allow me to write an entire Example and then get add the missing Class/methods. I am not taking this approach in the tutorial because it doesn't add that much time and I'd rather you see how it is supposed to be practiced.
Is this realistic? Yes. Many developers better than I have practiced the three laws. In the simulator mentioned above, during the week I was there, we strictly followed TDD. I was not always at the keyboard but the work was being projected on an 1080i overhead projector so that kept the developers "honest."
Check-In
You have all passing Examples, but two are not implemented. Should you check in your work?
Sure:
Macintosh-7% !g
git commit -a
Created commit f77e000: Added support for the "enter" function.
3 files changed, 148 insertions(+), 7 deletions(-)
Refactor
A quick review of RpnCalculator doesn't suggest a need for refactoring. For now, the same can be said of the Examples.
Check-In
No need, there was no need to refactor.
Summary
Do you think pressing <enter> on a calculator is a function? Before I started this project my reaction would have been, "That's silly." However, when I got to this Example, I considered the writeup on the RPN Calculator and realized that the <enter> key is a function just as much as sqrt, pow, etc.
Have we made progress? Yes, we have extended the API of the calculator. What about checking in code with pending Examples, is that a good idea? I think it is OK, but it could cause problems. I prefer clean Example execution, so having several pending Examples is a bother. On the other hand, if I'm pairing with someone and one of us thinks of something worthy of validation, but it is not where we are working, we could write it down, email it, yell it to another team member, or we can put it in the code in an executable fashion.
Example: Checking that the x_register is reset-able
The next Example involves what happens after the <enter> key. You could have included this check in the previous Example, however keeping the tests small, focused and checking fewer things makes pinpointing problems easier.
Example
Here's an example that seems to express the idea:
it "should cause next digit to reset the x_register"do
enter_number 654@calculator.execute_function(:enter)
enter_number 1@calculator.x_register.should == 1end
Create this Example and then run you Examples:
1)
'RPN Calculator Handling the <enter> key should cause next digit to reset the x_register' FAILED
expected: 1,
got: 6541.0 (using ==)
./rpn_calculator_spec.rb:41:
Finished in 0.023285 seconds
6 examples, 1 failure, 1 pending
Notice that the 1 entered was appended to the x_register. So looks like this test pushes our current production code. You might also have observed that the support method named "enter_number" is really a bit off. Since <enter> has a meaning now, this method's name should be changed. That, however, is a refactoring so you should wait until you have all Examples passing (it OK to leave the pending Example pending).
Update the calculator to work with the new Example:
def digit_pressed(digit)
@input = '' if @x_register_should_reset
x_register_should_reset = false
@input << digit.to_s
end
def execute_function(function_symbol)
@x_register_should_reset = true
end
Run your Examples, they should pass:
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack
- should cause next digit to reset the x_register
- should allow the x_register to be <entered> again (PENDING: Not Yet Implemented)
Pending:
RPN Calculator Handling the <enter> key should allow the x_register to be <entered> again (Not Yet Implemented)
Finished in 0.022283 seconds
6 examples, 0 failures, 1 pending
Check-In
Now is a good time to check in, even though you have a pending Example:
Macintosh-7% !g
git commit -a
Created commit d8902e3: Added resetting of x_register after :enter
3 files changed, 65 insertions(+), 3 deletions(-)
Refactor
Here's a list of things that you might consider for refactoring:
There's a violation of DRY between digit_pressed and initialize
There's some duplication between the last two Examples
The method name enter_number either needs to be changed, or it should call the <enter> method.
enter_number
You can either change its name or make it do what it says. Flip a coin, you probably do not have enough information to make that decision just yet. However, in the spirit of keeping things clean, I did make a decision. I kept the method name and changed its implementation:
def enter_number(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }@calculator.execute_function(:enter)end
Run your Examples, nothing is broken.
Violation of DRY between digit_pressed and initialize
This is one of those cases where duplication might be OK because right now initialize is simple but it is likely to get more complex later. I was originally going to change the implementation to this (don't do this):
However, before I made that change, it occurred to me that coupling to the initialize method will probably cause problems later. Even though I'd catch that when Executing my Examples, I instead decided to introduce a simple method that documents the intent.
Add a new method:
def reset_input
@input = ''end
Verify that nothing broke.
Update initialize and digit_pressed to use this new method.
Verify that nothing broke.
Duplication in last two Examples
When you changed the implementation of enter_number to actually perform an <enter>, you could have updated the last two Examples as follows:
it "should copy x_register to top of stack"do
enter_number 654@calculator.top.should == 654end
it "should cause next digit to reset the x_register"do
enter_number 654@calculator.digit_pressed1@calculator.x_register.should == 1end
When these Examples reflect the update to the enter_number method, the duplication does not see to be bad.
Check-In
Check in your work.
Macintosh-7% !g
git commit -a
Created commit a8fec92: Refactored input resetting, enter_number now
3 files changed, 75 insertions(+), 8 deletions(-)
Summary
The behavior of the calculator has grown. The it also is beginning to handle the <enter> functionality. You removed duplication in the production code. It was small, and trivial and probably seemed like nothing. However, here's a principle from Jerry Weinberg:
If you are not maintaining your code and constantly cleaning it up, then it is decaying. Even if your code is not changing, it is decaying because the market is changing, the users' understanding of your system is changing, something is changing.
Keeping your code clean is not an event, it's not a processes, it is an attitude. You have it or you do not have it.
Are you going to be perfect?
Perfection is not a destination, it is a journey.
No, you are going to miss stuff. That's why when you do find something, now is the time to handle it. If you cannot deal with it now, write it down on a piece of paper. Throw that piece of paper away in 2 days. If you haven't found the time to fix it by then, then by keeping the paper, you're just giving yourself undue stress.
Example: Should allow x_register to be entered again
This one has been pending. While I was just about to get to that, a few ideas went through my head:
There's going to be a stack of numbers somewhere.
Once the calculator starts resetting, the code never stops, so the Examples have left work on the plate.
Those two items are officially on the punch-list. For now, let's get back to all green. We've been away from all green for too long.
Example
Create the following example:
it "should allow the x_register to be <entered> again"do
enter_number 654@calculator.execute_function(:enter)@calculator.top.should == 654@calculator.x_register.should == 654end
Run your Examples:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling integers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack
- should cause next digit to reset the x_register
- should allow the x_register to be <entered> again
Finished in 0.022528 seconds
6 examples, 0 failures
Well everything is all green. So this Example might not be adding value. Rather than immediately delete it, keep a note to review it after completing the first two items on the punch-list.
Check-In
Should you check in? You might not be sure about the Example, but you've got that to review after just a few more Examples. Even so, go ahead and commit. If you remove the Example later, your RCS tool can handle the workload.
Commit:
Macintosh-7% !g
git commit -a
Created commit 437ee9f: Added candidate Example. Might remove it shortly,.
2 files changed, 51 insertions(+), 8 deletions(-)
Refactor
There are some duplicated validation steps in the Examples. Before refactoring, however, keep things as is until you've done just a little more work.
Check-In
There's nothing to check-in right now.
Summary
You've added an Example. You are not sure if it is a good one or not. You've got two items on your punch list. Review this Example after you have addressed those other two items.
Example: Resetting after the first digit
Once an example sends the execute_function method, the RpnCalculator will continue to reset. You need to fix this.
Example
Create the following example.
it "should only reset after the first digit and not subsequent digits"do
enter_number 654
enter_number 27@calculator.x_register.should == 27end
Run your Examples, notice the failure:
- should only reset after the first digit and not subsequent digits (FAILED - 1)
1)
'RPN Calculator Handling the <enter> key should only reset after the first digit and not subsequent digits' FAILED
expected: 27,
got: 7.0 (using ==)
./rpn_calculator_spec.rb:53:
Finished in 0.023869 seconds
7 examples, 1 failure
The problem is, the "reset state" is set in execute_function but never reset.
There are two ways to fix this: Update the digit_pressed method
Either will work. In fact, doing both works. Question, to which method does the responsibility seem to bind? If you tought reset_input, you're right.
Fix this by updating the reset_input method.
Make sure your Examples pass:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling integers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack
- should cause next digit to reset the x_register
- should allow the x_register to be <entered> again
- should only reset after the first digit and not subsequent digits
Finished in 0.025639 seconds
7 examples, 0 failures
Sidebar: I did too much
When I wrote "the solution" to the "should cause next digit to reset the x_register" Example, I put this logic in my code. I then realized that it was doing nothing to support any Examples so I removed it.
I then decided to create a test, the one you just worked on, to justify the need to reset the variable. If I had been working with a pairing partner, I'd hope my co-pilot would have called me on violating the third rule of TDD/BDD.
Check-In
Check in your work, you've made some good progress:
Macintosh-7% !g
git commit -a
Created commit e1a018b: Added Example to verify that resetting was turned off.
3 files changed, 139 insertions(+), 5 deletions(-)
Refactor
Reviewing the RpnCalculator there does not seem to be a need to do any refactoring. I do observe that the top method is still hard-coded, so an Example to drive that would be good.
As for the Examples, there is one thing you can do to clean up a little duplication. Every Example enters the number 654, so add a before method:
before(:each)do
enter_number 654end
Add the before method and run your Examples. Make sure all Examples still pass.
Now, remove the first enter_number 654 from each of the subsequent Examples.
Verify that all of your Examples still pass.
Check-In
With the one refactoring, now is a good time to check in your changes
Summary
You've pushed the solution a bit further and cleaned up some duplication. Specifically, resetting the value in the x_register seems to work and you've put some common setup work into a before for your Context.
You still have the question of whether there should or should not be a stack. And what about that pesky Example? You're keeping a punch-list, right?
Example: Enter actually puts values on the stack
Up to this point, your examples have addressed the effect of the <enter> key on the x_register, but what about stored values? Consider the following:
7 <enter>
14 <enter>
99
What can you say about the RpnCalculator?
The x_register has a value of 99.
The x_register can still receive receive digits.
The first previous value is 14.
The second previous value is 7.
You already have validated the first two bullets with previous Examples, so there is no value in re-checking those features. Youcould add validation to existing examples to check for the second two values. I would advise against doing so because smaller tests tend to pinpoint problems better. Also, depending on the technology you are using, once a validation fails, subsequent validations are not checked in in RSpec, nor are the in most testing tools (FitNesse being one counter example).
Why is this last part important? When you've broken something, how big is the break? If your Examples fail, but each has one or a very small number of validations, you'll have a good idea of the span of the break. However, if you have Examples with dozens of validations (or many more), it is possible that an early validation will fail and you simply do not know if other validations would have failed. So while you think you only broke one thing, you broke 30 things. With many small tests, you'll have much higher fidelity information from which to move forward.
So with that in mind, you can continue with one a few more Examples working with <enter> to move towards validating the last two bullets.
Example
Here is an example that moves your production code just a little forward:
it "should have two operands available after one <enter>"do@calculator.available_operands.should == 2end
This Example suggests that after the first <enter> you should have two operands available, the one on the stack and the x_register.
Create this example.
Add the missing method with an empty implementation.
Get the example to pass:
def available_operands
2end
Verify that all of your Examples now pass.
Check-In
Check in your work.
Refactor
There was little added to either of your two files, so there's not much to refactor right now. However, did you notice that you now have two hard-coded methods? This might suggest a direction in which you want to push your Examples.
Check-In
No need, no change.
Summary
This Example really just extended the API of the RpnCalcualtor. You might be feeling like it is time to get busy because of the two pending methods.
Example: Enter should increase the number of operands
The operator_count is hard-coded, this Example will make it harder to get away with that.
Example
it "should increase the operand count after each <enter>"do
enter_number 13@calculator.available_operands.should == 3end
Create this Example. Notice that since you added no new API calls to the RpnCalculator's interface, you can write the complete Example while following the second law of TDD/BDD.
Verify that your Example fails:
- should increase the operand count after each <enter> (FAILED - 1)
1)
'RPN Calculator Handling the <enter> key should increase the operand count after each <enter>' FAILED
expected: 3,
got: 2 (using ==)
./rpn_calculator_spec.rb:62:
Finished in 0.026028 seconds
9 examples, 1 failure
There are two ways to make this work. Here's one way to get this to pass (don't do this): Update initialize
Alternatively, you can use an array and actually store the values entered (notice, you change the same methods, add the same amount of code): Update initialize
def initialize
reset_input
@operands = []end
Update execute_function
def execute_function(function_symbol)@x_register_should_reset = true@operands<< x_register
end
Update operand_count
def available_operands
@operands.length+1end
Which of these is the least amount of code that will get the Example to pass? Both involve 3 lines of code added to 3 methods. One uses an integer, one uses an array, and an array is more complex than an integer, though in Ruby they are both objects.
Why do you try to do the least amount possible to get an Example to pass? That guideline is a "how" not a "what or a why". That is, it is a means to an end. What end? There are at least a few:
Writing only enough code means you are less likely to over-design or gold-plate your solution.
It also helps keep the production code actually covered by tests.
In this case, the Example will exercise all of the added code in both approaches. What about over-design? When are you over designing? Are you over designing when you are using a design pattern where just a simple direct relationship is adequate? Yes. Are you over-designing when you a complex object structure, when a small, flat structure will suffice? Yes. Is over designing always a problem? NO. The problem is not over design. The problem is that over design leads to more rework when you've guess wrong. And as a developer, I can say that I guess wrong often.
Why do developers guess wrong? Because we generally see problems differently that people who provide requirements. Here's an analogy. On the Nintendo WII, there's a feature called "Mii's". A Mii is an avatar. I only created one because I had to for some games. I created one and never change it. My wife and daughter both have crated more than one, they update their looks and I'm convinced if there was a game that simply let players updated their Avatars with clothes, and makeup, etc, they'd buy it. If you asked me, it's a waste of time. But that is one of the may appeals to the system.
Problems change in ways that we often cannot anticipate simply because they are coming from a very different place. So when you write more code than is necessary at the moment, you're risking having to change code, or maybe make something that is designed for unnecessary flexibility, which could make the code harder to adapt to actual changes on the ground.
In this example, the domain is somewhat technical. Also, you can read the manual for RPN calculators and they will talk about a stack. So using an array as this code does is not diverging from the problem domain.
The second solution is a good one and it is not over design given the domain context.
Implement the second solution.
Verify that all of your Examples pass.
Check-In
Check in your work.
Refactor
A quick review of the code shows a hard-coded top method. Now that you have a stack in place, can you simply return the "top" of the array?
Experiment, change the implementation of top:
def top
@operands.last
end
Run your Examples, everything still passes.
This works. You have changed the production code without breaking any tests, so this is a legitimate refactoring.
Check-In
Check in simple refactoring.
Summary
You have moved towards a more complete solution by introducing an array to hold operands. Was this over design? No. You certainly could have used the integer and then the stack, but that seems somewhat dogmatic.
You also managed to get rid of the last hard-coded method, top. This one Example managed to move the production code forward quite a bit.
Example: The right thing is on the stack
Right now, there's no way to know if what is on the stack is the right thing on the stack. Most of the pieces are in place, in fact even though the code is fully covered by the Examples, the Examples do not reflect fully what your production code can do. It is time to exploit that.
Example
Here is an example that verifies the stack behavior of the RpnCalculator:
describe "Stack"do
it "should contain the correct items in the correct order"do
enter_number 4
enter_number 19
enter_number -1@calculator.pop!.should == -1@calculator.pop!.should == 19@calculator.pop!.should == 4endend
First observation: This code will not run because there is no pop! method on the RpnCalcualtor.
Create the Example, verify that the test fails as expected.
Add an empty pop! method to verify that the Example fails for the right reason:
def pop!
end
Run your Examples.
Implement pop!:
def pop!
@operands.popend
Verify that your Examples pass.
Check-In
Check in your work.
Refactor
There has been a slowly-growing code small, Feature Envy, but hold off until just a bit longer. The "describe" part of the last Example is a huge clue.
Check-In
No need, you did not do any refactoring.
Summary
You have added an example to verify that in fact the right values are getting put onto the stack. Your RpnCalculator is growing, but at what point is the RpnCalcualtor suffering from too much design debt? It is right now, but you'll see that after the next Example.
Example: What happens with an 'Empty' stack?
The stack on an RpnCalculator is conceptually filled with 0's when it has no values. In fact, it is literally filed with 0's. So if you check the top of your RpnCalculator, calculator, it should return 0 when it is empty. The same is true for pop!. It should return 0 if the stack is empty.
Example
These two are closely related, so here, for the first time, are two complete examples that you will fix at the same time.
it "should return 0 when pop!'ing from an empty stack"do@calculator.pop!.should == 0end
it "should return 0 for top when stack empty"do@calculator.top.should == 0end
Create these Examples, they will both fail the same way:
- should return 0 when pop!'ing from an empty stack (FAILED - 1)
- should return 0 for top when stack empty (FAILED - 2)
1)
'RPN Calculator Stack should return 0 when pop!'ing from an empty stack' FAILED
expected: 0,
got: nil (using ==)
./rpn_calculator_spec.rb:77:
2)
'RPN Calculator Stack should return 0 for top when stack empty' FAILED
expected: 0,
got: nil (using ==)
./rpn_calculator_spec.rb:81:
Finished in 0.031744 seconds
12 examples, 2 failures
When code pop's values off of an empty array, the array returns nil. The same thing happens when code calls top. This will fix both of your failing Examples:
Make these changes and verify all of your Examples are passing.
Check-In
Check in, it is finally time to get busy removing Feature Envy and get closer to conforming to the Single Responsibility Principle.
Refactor
Review the following methods: pop!, top, available_operators. What do they all have in common? All three of these methods deal exclusively with the @operands instance variable.
Is this bad? Yes, it suggests that the RpnCalculator is doing work that should be pushed into the array. What? That's right, the array is not pulling its weight. This is a legitimate case for wrapping a collection class and giving it domain-specific behavior (semantics).
There is a name for methods in classes working exclusively with data in other objects. It is a code smell called Feature Envy.
Normally, at this point I'd say "you cannot change a system-defined class, so you either need to subclass or wrap and adapt". That is, create a subclass of Array, change the top method and add the pop method. Or, create a new class called something like OperandStack that holds a single instance of an array.
However, in Ruby, classes are never closed. You can add methods to a class anytime. Adding methods to Array is a bad idea since the Array class is used all over the place. However, Ruby also allows you to add methods to an instance.
This is crazy. Here is an example of how you could do that (don't do this):
The result from running this will be "true" printed twice. But if you try to simply create an array, you will not see the same results. Neither method is defined on the Array class.
Barring extending a single instance, it is certainly reasonable to create an OperandStack class, move these methods into it and then change the implementation of the RpnCalcualtor to use the OperandStack.
Before you start, did you notice that the Context for the last three Examples was "Stack"? That's a big clue that there's a missing class.
There are 3 Examples in the "Stack" context. Move the first one out into "Handling the <enter> key" context.
Verify that all of your Examples are still passing.
Next, you are going to do some "big" changes. It won't take long, but this will be the longest amount of time the tutorial will have you work before you can see your Examples running. Note your reaction to the change.
Change the structure of rpn_calculator_spec.rb:
describe "RPN Calculator"do
...
end
describe "Stack"do
...
end
If you ran your examples now, the ones in the Stack context will fail. Why? Because they no longer pick up the automatically-created calculator object form the before in the "RPN Calculator" context.
Update the "Stack" context:
describe "OperandStack"do
before (:each)do@stack = OperandStack.newend
it "should return 0 when pop!'ing from an empty stack"do@stack.pop!.should == 0end
it "should return 0 for top when stack empty"do@stack.top.should == 0endend
This still won't pass. There's no OperandStack class.
Move the entire OperandStack context into a new file called "operand_stack_spec.rb".
Now your rpn_calculator_spec will pass:
Macintosh-7% spec -f s -c rpn_calculator_spec.rb
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling integers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack
- should cause next digit to reset the x_register
- should allow the x_register to be <entered> again
- should only reset after the first digit andnot subsequent digits
- should have two operands available after one <enter>- should increase the operand count after each <enter>- should contain the correct items in the correct order
Finished in0.027715 seconds
10 examples, 0 failures
Next, create a new class called OperandStack in a file called opeand_stack.rb:
class OperandStack
def initialize
@operands = []enddef top
@operands.length>0 ? @operands.last : 0enddef available_operands
@operands.length+1enddef pop!
@operands.length>0 ? @operands.pop : 0enddef<<(value)@operands<< value
endend
Note that these are mostly just copies of methods from RpnCalculator. There is one new method, <<, which just delegates to a method of the same name.
Verify that your operand_stack_spec.rb now passes:
Macintosh-7% spec -f s -c operand_stack_spec.rb
OperandStack
- should return 0 when pop!'ing from an empty stack
- should return 0 for top when stack empty
Finished in 0.013598 seconds
2 examples, 0 failures
Did you fully test this class? No. What you instead did was extract a class and then copy the tests that were directly testing it. You are not writing new code, you are refactoring the solution.
Run all of your Examples, everything should be passing:
Macintosh-7% spec -f s -c *_spec.rb
OperandStack
- should return 0 when pop!'ing from an empty stack
- should return 0 for top when stack empty
RPN Calculator
RPN Calculator Basic Creation
- should be non-null after creation
RPN Calculator Handling integers
- should store a series of digits in the x register
- should handle a string of digits with an embedded .
RPN Calculator Handling the <enter> key
- should copy x_register to top of stack
- should cause next digit to reset the x_register
- should allow the x_register to be <entered> again
- should only reset after the first digit and not subsequent digits
- should have two operands available after one <enter>
- should increase the operand count after each <enter>
- should contain the correct items in the correct order
Finished in 0.029495 seconds
12 examples, 0 failures
Intra-refactoring Check-In
All of your Examples are passing, right? Now is a great time to check in before moving to the next step.
Back to refactoring
Now you should refactor the RpnCalcualtor to use your newly-created class. As with other refactorings, you'll add, duplicate, validate and then remove code.
Add "require 'operand_stack' to the beginning of your rpn_calculator.rb file.
Update execute_function to duplicate the work of storing the x_register:
def execute_function(function_symbol)@x_register_should_reset = true@operands<< x_register
@operand_stack<< x_register
end
Run your Examples, everything should still pass.
Update top:
def top
@operand_stack.topend
Run your Examples, everything should still pass.
Update pop!:
def pop!
@operand_stack.pop!
end
Reviewing available_operands, you notice that there's no length method on Operand stack. So add it:
def length
@operands.lengthend
Run your Examples, everything should still pass.
Update available_operands:
def available_operands
@operand_stack.length+1end
At this point, you can remove all code that refers to @operands (you'll change 2 places).
Run your Examples, everything should still pass.
Check-In
Whew! That was a big refactoring. Check your code in!
Summary
This was a big change. You had a violation of feature envy. The RpnCalculator was doing a lot of work with information stored in an Array. So you created a class to represent that work, OperandStack, and moved Examples around to get everything situated.
If you have a decent IDE, these kinds of refactorings are quick, easy and fairly reliable. If not, then like Robert Martin tweeted:
It's like riding a horse on the interstate.
This cleaned up your RpnCalculator class but there's still more to be done. The implementation of execute_function is wanting to do more (or less).
Is there a problem with how you are testing OpernadStack? There are only 2 Examples but quite a few methods for which you do not have unit tests. Those methods are in fact used from the RpnCalculator so you have coverage. However, if the OperandStack is used by other classes, then the testing is not adequate. Maybe you should consider a nested class. Maybe you should write more Examples to get its "contract" fully specified.
When you are just learning TDD or BDD, this kind of stuff is going to happen. Let's leave it alone for now and see what happens.
Sidebar: Wrapping Collections
Do it!
I was tempted to just leave it at that, but I'll say a bit more. In my experience, most (anything beyond a trivial problem and most trivial problems) of the time I use a collection, at some point I'll want more behavior that the collection offers. The collection implements a "raw" zero-to-many relationship. Most of the time, my requirements want more than just a "raw" relationship. In the case of the RpnCalculator, it's stack is of infinite size and it always has zero after anything else that is pushed onto it. We simulated this by returning 0 if the stack is in fact empty.
It is nearly always a good idea to augment a system-defined collection with your own flair.
When you do, you have several options:
Subclass (in .Net you don't have this option because most methods are sealed and non-virtual)
Wrap and delegate (this is what we did). You always have this option
Open up the class (Ruby and other dynamic languages) - this is dangerous. Opening up a heavily used class like this is "too cool."
Open up an instance (shown above) - this is probably too cool as well.
The option that always works, though it might require a touch more work, is the second option and what you did in this tutorial.
If you every have a collection within another collection, it's immediately time to follow this recommendation.
The Classes So Far
Here's one example of what all of your code should look like about now. rpn_calculator_spec.rb
require'rpn_calculator'
describe "RPN Calculator"do
before(:each)do@calculator = RpnCalculator.newenddef enter_number(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }@calculator.execute_function(:enter)end
describe "Basic Creation"do
it "should be non-null after creation"do@calculator.should_not be nilendend
describe "Handling integers"do
it "should store a series of digits in the x register"do
enter_number 42@calculator.x_register.should == 42end
it "should handle a string of digits with an embedded ."do
enter_number 13.31@calculator.x_register.should == 13.31endend
describe "Handling the <enter> key"do
before(:each)do
enter_number 654end
it "should copy x_register to top of stack"do@calculator.top.should == 654end
it "should cause next digit to reset the x_register"do@calculator.digit_pressed1@calculator.x_register.should == 1end
it "should allow the x_register to be <entered> again"do@calculator.execute_function(:enter)@calculator.top.should == 654@calculator.x_register.should == 654end
it "should only reset after the first digit and not subsequent digits"do
enter_number 27@calculator.x_register.should == 27end
it "should have two operands available after one <enter>"do@calculator.available_operands.should == 2end
it "should increase the operand count after each <enter>"do
enter_number 13@calculator.available_operands.should == 3end
it "should contain the correct items in the correct order"do
enter_number 4
enter_number 19
enter_number -1@calculator.pop!.should == -1@calculator.pop!.should == 19@calculator.pop!.should == 4endendend
require"operand_stack"
describe "OperandStack"do
before (:each)do@stack = OperandStack.newend
it "should return 0 when pop!'ing from an empty stack"do@stack.pop!.should == 0end
it "should return 0 for top when stack empty"do@stack.top.should == 0endend
operand_stack.rb
class OperandStack
def initialize
@operands = []enddef top
@operands.length>0 ? @operands.last : 0enddef available_operands
@operands.length+1enddef pop!
@operands.length>0 ? @operands.pop : 0enddef<<(value)@operands<< value
enddef length
@operands.lengthendend
Example: A Binary Operator
Some time ago you created your first Example to exercise the <enter> key. That resulted in the following method:
def execute_function(function_symbol)@x_register_should_reset = true@operand_stack<< x_register
end
Now it is time start executing more functions. The first function you'll support is +.
Example
Here is a basic example to get started:
describe "Basic Math Operators" do
it "should add the x_regiser and the top of the stack" do
enter_number 46
@calculator.execute_function(:+)
@calculator.x_register.should == 92
end
end
Why is the result 92? The support method enter_number
Create this Example and run it. The result is not quite right:
1)
'RPN Calculator Basic Math Operators should add the x_regiser and the top of the stack' FAILED
expected: 700,
got: 46.0 (using ==)
./rpn_calculator_spec.rb:79:
Finished in 0.03118 seconds
13 examples, 1 failure
Check-In
Check in your code. You are about to refactor.
Refactor
OK, the method you just updated, execute_function, is a bit of a mess. It does two thigns:
Determine which function to perform
Actually perform the work.
So you'll separate this for now.
Extract two methods, enter, +:
def enter
@x_register_should_reset = true@operand_stack<< x_register
enddef+@input = 92.to_send
Make sure your Examples still pass.
Update the execute_function to use those two methods:
def execute_function(function_symbol)if function_symbol == :enter
enter
elseself.+endend
Update your RpnCalculator, verify that your Examples are all passing.
Check-In
This is all the refactoring you'll do right now. More is on the horizaon.
Summary
You've improved the execute_function but you have a stubbed-out + method. If you know a bit of Ruby, you might be chomping at the bits to evaluate a symbol, or use lambda and a hash. The code will get where it will, however, based on the Examples, rather than speculation.
Example: Work on +
What should happen with + when there are no operands? Should it fail? Should it result in 0? If you're simulating an HP calculator, the result is 0. However, what it should do will be defined by the Examples you write.
Example
it "should result in 0 when the calculator is empty"do@calculator.execute_function(:+)@calculator.x_register.should == 0end
Create this Example. Verify that it fails.
Update the + method to actually do some work.
def enter
@x_register_should_reset = true@operand_stack<< x_register
end
Run your Example, make sure it works.
Check-In
Check in your work.
Refactor
Something is starting to look strange. You may have noticed it the first time your code "supported" +. The following line:
@input = result.to_s
I'm not sure what to do with that yet because there's not enough code to make many decisions. It might be that the code will stay put. It just seems a bit strange.
Check-In
No refactoring done, no need to check in again.
Summary
The + method no does work. It has one strange line, and there's still the issue of whether it modifies the stack correctly or not. Another thing occurred to me as well. After any operator, the x_register should be in "reset" mode.
Example: x_register in reset mode after +
Like <enter>, after perform +, the next character typed should clear the x_register and write into it. That seems like a good thing to check next.
Example
it "should reset the x_register after +"do
enter_number 99@calculator.execute_function(:+)@calculator.digit_pressed8@calculator.x_register.should == 8end
Create this Example. Run it and you'll notice that it fails:
1)
'RPN Calculator Basic Math Operators should reset the x_register after +' FAILED
expected: 8,
got: 9.08 (using ==)
./rpn_calculator_spec.rb:91:
Finished in 0.032479 seconds
16 examples, 1 failure
Update the enter and + methods to call this new method.
Make sure your Examples still pass.
Check-In
Even though small, you did make a change to your solution while keeping your Examples passing, so check in your work.
Summary
There appears to be a pattern emerging. Two functions, enter and +, both of which change the calculator into "override_mode". Will other functions look like this? Yes. Will you be able to remove this duplication as well? Yes.
Example: What about the stack?
Some time back the tutorial asked about whether the stack was treated properly. You could have put those checks into another Example, but the rationale for not doing that is to keep your tests small and focused for better pinpointing of problems and better assessment of the size of a break.
It's time to come back to that, if for no other reason than to expression a "contract" that the + method (and all binary operators) will need to follow.
Example
Here's an example:
it "should should reduce the stack by one"do
enter_number 9
enter_number 8@calculator.execute_function :+@calculator.available_operands.should == 2end
Create this example.
Run your Examples. Notice it passes.
Did we code too much or are we validating a behavior? Would it have been possible to write less code and still keep other tests passing? Maybe, the design is such that it's getting harder to do the wrong thing. In any case, this seems like an OK Example - if maybe a bit off target.
Check-In
Check in your work.
Refactor
There's starting to be a bit of cruft in the Examples and maybe something in the RpnCalcualtor. You might already be noticing yourself. For example, in RpnCalculator, to set the "x_register" the code does the following:
@input = result.to_s
set_override_mode
That is not self evident. In addition, getting two operands for a binary operator involves two very different expressions:
lhs = @operand_stack.pop!
rhs = x_register
The last two Examples are OK, though do you believe that the differences between digit_pressed and enter_number is obvious?
Start by making an improvement to the bottom part of the + method:
Add a new method that is private (since private sets subsequent methods, make this the last method in your RpnCalculatorClass):
private
def reset_x_register(value)@input = value.to_s
set_override_mode
end
Verify your Examples still pass.
Update +:
def+
lhs = @operand_stack.pop!
rhs = x_register
result = lhs + rhs
reset_x_register result
end
Verify your Examples still pass.
Check in this refactoring.
The next thing you might consider is how to better document getting parameters for +. I tried a few ways but none seemed to really make the code better. Just differently convoluted. You might try yourself (and even succeed). However, I already have an idea of where I want this code to go, so rather than any more refactoring, I want to move into other operators.
Check-In
Check in your refactoring.
Summary
You now have the basics of the + operator worked out. You make resetting the x_register at least a little better. Now it is time to add some more operators to see how this current solution grows.
Example: Support for a new binary operator, -
Adding support for - will allow you to see how your solution grows as new math functions are supported. You have already vetted + fairly well, so adding - should be a snap.
Example
it "should subtract the first number entered from the second"do
enter_number 4@calculator.digit_pressed9@calculator.execute_function :-@calculator.x_register.should == -5end
At this point, hopefully you are getting bothered by the difference between lines that start with an @ and ones that do not. Add that to your punch-list.
Create this example. Run your examples and verify that they do not pass.
Were you surprised by the result? The results suggest that instead of -, your calculator performed +. Why is that?
A quick review of execute_function should give you a clue:
def execute_function(function_symbol)if function_symbol == :enter
enter
elseself.+endend
So if something is not :enter, then it is :+. Add that to your punch-list:
Your execute_function should fail if the operator is not found.
However, you are not working on that Example. So update this method to call -:
Now your code is missing the function - (which you discovered by running your Examples, right?), so add it:
def-end
Run your Example, now it should be failing for the right reason (value did not match).
You have two obvious choices here:
Create a method that is hard-coded to return the correct value.
Create a duplicate of the + method and change it to be -
Rather than following the third rule, you can safely duplicate the + method and update it (you'll be refactoring this soon anyway):
def-
lhs = @operand_stack.pop!
rhs = x_register
result = lhs - rhs
reset_x_register result
end
Verify that all of your Examples are passing.
Check-In
Check in, you have a lot on your punch-list.
Refactor
There are several things on your punch-list:
Adding a new operator requires too much work: update a method, add a new method, and write that method. More importantly, it requires changing an existing method which is in direct violation of the open/close principle.
The execute_function does not indicate if the function does not exist.
There is duplicated code between the + and - methods. (This was on your list, wasn't it?)
The difference between enter_number and digit_pressed seems isn't obvious.
The Examples should not have such a mix of lines that use fields and support methods.
Looks like you have a lot of work.
First, if you have been chomping at the bits to fix execute_function, now is the time to do so:
Looks like you've managed to fix the first three things on your punch-list with a few quick changes in your code. Of course, you still do not have an Example that captures what happens when you execute a bogus method, the code should handle that now.
Before calling this refactoring session done, clean up your Examples to make this a bit more self-explanatory.
Add some new support methods (put these at the same place as enter_number):
def type(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }enddef press(a_symbol)@calculator.send(a_symbol)enddef validate_x_register(expected)@calculator.x_register.should == expected
end
Make sure your examples still pass.
Update "should subtract the first ...":
it "should subtract the first number entered from the second"do
type 4
press :enter
type 9
press :-
validate_x_register -5end
Run your Examples, they should all pass.
Sidebar: Example styles and Your audience
Which do you prefer? You have seen three different forms:
First, you saw Examples simply using the calculator.
Second, you saw Examples using a combination of support methods and using the calculator.
Now, you see an Example only using support methods.
The first form gives a concrete example of how the RpnCalculator should be used. The last form makes the Example easy to read and moves the writing of the examples into the way a user might speak about using the calculator.
Here is just he Basic Math Operators context updated:
describe "Basic Math Operators"do
it "should add the x_regiser and the top of the stack"do
type 46
press :enter
press :+
validate_x_register 92end
it "should result in 0 when the calculator is empty"do
press :+
validate_x_register 0end
it "should reset the x_register after +"do
type 9
press :+
type 8
validate_x_register 8end
it "should should reduce the stack by one"do
type 9
press :enter
type 8
press :enter
press :+@calculator.available_operands.should == 2end
it "should subtract the first number entered from the second"do
type 4
press :enter
type 9
press :-
validate_x_register -5endend
Here's a side-by-side comparison between the two styles:
it "programmer" do
@calculator.digit_pressed 4
@calculator.execute_function :enter
@calculator.digit_pressed 5
@calculator.digit_pressed 2
@calculator.execute_function :+
@calculator.x_register.should == 56
endit "user" do
type 4
press :enter
type 52
press :+
validate_x_register 56
end
Before thinking that one style is the "right" style, consider the audience and even the author. If the audience is the user, then the style on the right is more appropriate. If the audience is another developer, then the left side might be more appropriate.
You might think that using the methods names on the right for your calculator is an option. However, that won't look correct:
The method names do not make sense being sent to a calculator. So the method names make sense on the left when sent to a calculator. The method names on the right make sense when reading an example.
While this tutorial will not cover it, RSpec has the notion of "story tests", or tests that are meant to be written by the same people who write user stories, your customer, product owner, QA person or even developers. That's coming up in a later tutorial, so this subject will come up again.
However, just to place a (somewhat arbitrary) stake in the ground, here's an updated version of rpn_calculator_spec.rb:
require'rpn_calculator'
describe "RPN Calculator"do
before(:each)do@calculator = RpnCalculator.newenddef type(number)
number.to_s.each_char{|d|@calculator.digit_pressed d }enddef press(a_symbol)@calculator.execute_function a_symbol
enddef validate_x_register(expected)@calculator.x_register.should == expected
enddef validate_top(expected)@calculator.top.should == expected
enddef validate_operands(expected)@calculator.available_operands.should == expected
enddef validate_pop!(expected)@calculator.pop!.should == expected
end
describe "Basic Creation"do
it "should be non-null after creation"do@calculator.should_not be nilendend
describe "Handling integers"do
it "should store a series of digits in the x register"do
type 42
press :enter
validate_x_register 42end
it "should handle a string of digits with an embedded ."do
type 13.31
validate_x_register 13.31endend
describe "Handling the <enter> key"do
before(:each)do
type 654
press :enterend
it "should copy x_register to top of stack"do
validate_top 654end
it "should cause next digit to reset the x_register"do@calculator.digit_pressed1@calculator.x_register.should == 1end
it "should allow the x_register to be <entered> again"do
press :enter
validate_top 654
validate_x_register 654end
it "should only reset after the first digit and not subsequent digits"do
type 27
validate_x_register 27end
it "should have two operands available after one <enter>"do
validate_operands 2end
it "should increase the operand count after each <enter>"do
type 13
press :enter
validate_operands 3end
it "should contain the correct items in the correct order"do
type 4
press :enter
type 19
press :enter
type -1
press :enter
validate_pop! -1
validate_pop! 19
validate_pop! 4endend
describe "Basic Math Operators"do
it "should add the x_regiser and the top of the stack"do
type 46
press :enter
press :+
validate_x_register 92end
it "should result in 0 when the calculator is empty"do
press :+
validate_x_register 0end
it "should reset the x_register after +"do
type 9
press :+
type 8
validate_x_register 8end
it "should should reduce the stack by one"do
type 9
press :enter
type 8
press :enter
press :+@calculator.available_operands.should == 2end
it "should subtract the first number entered from the second"do
type 4
press :enter
type 9
press :-
validate_x_register -5endendend
Get to where all of your Examples are passing.
Check-In
Check in your work.
Summary
That was a lot of refactoring. Maybe the tutorial let you collect too much design debt. However, this will happen and it is up to you to pick up the slack.
You made several important changes:
The execute_function method now uses the send method to execute a symbol, so you will not have to update it so long as new functions are added to the RpnCalculator clss.
As a result of this change, your execute_function will indicate errors better than when you added an Example for - (which caused + to get called)
You created an execute_binary_operator method that makes adding binary operators a one-line lambda expression.
As a result, you removed some DRY violations.
You made a significant change to how you write your examples.
What's left? There's still the issue of documenting what happens when you attempt to execute a missing function and the rpn_calculator_spec.rb file is getting large.
This second issue is not addressed in this tutorial.
Example: Handling missing functions
So what happens when your calculator is asked to execute an unknown function?
Example
describe "Handling Functions"do
it "should raise an exception if attempting to run missing function"dolambda{ press :bogus_function_name}.should raise_error NoMethodErrorendend
Create this new Context and verify that it passes.
This Example documents that you expect the behavior of throwing a NoMethodError when sending a bogus method name to your calculator.
Check-In
Check in your code.
Refactor
You just did a bunch of refactoring. You only added an Example to capture something that was added during refactoring but not explicitly spelled out in any Example. So nothing new to refactor.
Check-In
You have no changes to check in.
Summary
This example really just captured something about your system that was not explicitly spelled out anywhere. This probably indicates too much change done during refactoring. It would have been possible for you to wait to change the implementation of execute_function and first write this Example.
You won't always figure out to do that, so you've managed to recover gracefully.
Example: Add support for * and /
You have a working design for + and -, adding * and / should be a snap. First your calculator will support multiplication.
Example
it "should multiply the first number entered by the second"do
type 3
press :enter
type 9
press :*
validate_x_register 27end
Create this example. Run it and verify that your test fails with a missing method.
Refactor
You might review your code and find some things to refactor. One issue is the size of the calculator. Right now it is not too bad, but it's going to get bigger. Each time you add a function, it grows.
Check-In
Nothing new to check in.
Summary
You've just finished out the basics for your calculator.
RUNNING OUT, COPY AGAIN
Example x
Overview Example Check-In Refactor Check-In Summary
Example x
Overview Example Check-In Refactor Check-In Summary
Introduction
Table of Contents
As with the first tutorial, this tutorial covers:
Unlike the first tutorial, this tutorial will:
By the end of this tutorial, you'll be ready to start using story tests on a more complicated problem.
Note
This tutorial assumes you've either already worked through the first tutorial or are familiar with BDD and RSpec. If not, here are a few terms that come up early enough you'll need to know them:Approach
Unlike the first tutorial, this tutorial will be a bit more explicit in its steps. Where reasonable, each Example will follow the same flow:There will also be occasional side-bars on an as-needed basis.
Here are Robert Martin's three laws of TDD written from a BDD perspective:
These rules are not enough to effectively write code using either BDD or TDD. Here are a few more things to take into consideration:
Before you get started on any Examples, however, we'll delve into the problem just a bit.
The Problem
An RPN calculator works by performing calculations on operands that the user has already entered. Here is an example scenario:In this example, a user enters first 5 and then 3 and hits the + key to get the previous two operands 5 and 3, added together.
In general, using an RPN calculator obviates the need to use parenthesis. Consider the following:
That same expression could be calculated on an RPN calculator as follows:
An RPN calculator as a stack of previously entered values. The most recently entered numbers are the numbers that become available first (Last In First Out - LIFO). Consider this longer sequence:
Here's the evaluation of the above expression:
The x register
The x register behaves like an accumulator. As you type numbers, those numbers get added to what is already there. When you type a non-number key, it applies itself. For example, the <enter> key really behaves like a unary operator. When you press <enter>, it takes the value in the x register and places it on the stack.After any non-number key, the x register is "locked" and any attempt to change it results in it being reset.
Getting Started
There are several major parts to this problem:In addition, we'll add several things to make this a bit more interesting:
Behavior Driven Development advocates starting outside in; start with the user experience and then work your way into the system. In this tutorial, you will not strictly be creating a user interface; you will, however, create the stuff necessary to build a user interface. The tutorial will start at the individual entry of keys and work up to basic math.
Your Work Area
The 0th Example
As with all problems, you need to pick a starting place. As with the first tutorial, you will create a basic example describing object creation. Next, you'll continue with a series of examples centered on basic input. Thinking further ahead at this point might constitute analysis paralysis.Example
Here is a first Example for this problem:
- Execute the Example, it fails:
The -c option adds color to the output. You won't see the color in this tutorial. Also, if you are running this in a DOS terminal window, then you'll want to leave this option off.The message describes an unknown constant, RpnCalcualtor. You'll need to create that class.
OK, you have an Example but it does not contain any verification. So one more change and one more execution.
Generally, we want Examples to fail for an expected reason before they pass. This case is an exception to that rule, but you'll see that coming up.
All green refers to the state of the executing Example; it means all Examples are executing and passing. This terminology derives from unit testing in Smalltalk and then popularized by JUnit. In Smalltalk, their process was:
This is essentially what this tutorial advocates. JUnit turned this into red-bar, green-bar. When the GUI of JUnit executes, it has a progress bar that indicates either:
Check-In
You are all green, so it is time to check in your work. As with the previous tutorial, I'll be using git:
Note that in this example I have a file you will not have, wikiwriteup.txt. That's the raw material used for format the wikipage you are now reading.
Refactor
A quick review of the existing Example and production code does not suggest any refactoring yet.
Check-In
Since there was no need to refactor, there's no need to check in again.
Summary
You created two files:
rpn_calculator_spec.rb
rpn_calculator
This may seem like a trivial start, and that is by design. What have you verified so far:
Not every Example will be so small and quick to create, but you should attempt to break your work down into bite-sized chunks like this. Why?
In mid 2008, I worked with a great group of people on the East Coast. My task was to use their current problem as source material and to take them through TDD (not BDD) on their problem.
They already had a start on the problem and had pretty good source material on what the system needed to do. The system essentially simulated expensive hardware. This system would make testing the software to drive the expensive hardware. The problem involved capturing an over-the-wire protocol, which they were already doing in C++. The problem, then, was to take that protocol and respond appropriately and even initiate messages.
During the week I was there, we:
On Tuesday, the week after I left, they had managed to fix a few problems on protocol capture and had successfully processed a job. On Wednesday, they were successfully processing a number of simple jobs.
A few weeks later they were processing complex jobs and then a few weeks later they were processing up to 15 simultaneous complex jobs. Their simulator was fast enough that they had to slow it down.
In a very real sense, starting with a granule and letting the system grow through accretion is a viable way to build complex systems.
Example: Accepting User Input
With the first Example running it is time to move on to basic user input. We have to make a decision already, where is the system boundary. Here are a few possibilities:There are other alternatives, and to some extent, the selection is arbitrary. There is no decision on the UI technology there's no way to know the eventing mechanism, if applicable. Even so, you can make progress based on some logical representation and then adapt between the real UI technology and the system you've developed.
A standard model for handling user input is the Model:View:Control pattern. In this case, the View is the as-yet-to-be-determined presentation technology. The Model is the calculator itself. The control part of the equation maps between the view and the model (the UI and the calculator). So you will create the model, drive the system using RSpec as the de-facto control, and when you've selected the UI technology, you'll create a new control to map from the UI to your well-tested model.
As a side benefit, most of the logic will be in easy to test code.
Since the particulars of the UI are irrelevant, this tutorial will move forward using option 2 listed above for handling numbers. When it is time to handle things like +, /, etc., it'll be time to make another decision.
Example
Here is the beginning of an example:
There's more to be written, but as soon as the message "digit_pressed" is sent to a calculator, the Example will fail.
Assuming you worked through the first BDD tutorial, this is familiar ground. You need to add a method to get this to compiled (you've just finished applying law 2 of the 3 laws of BDD).
As with the previous failure, this failure is a result of a missing method.
1) 'Handling User Input of Numbers should store a series of digits in the x register' FAILED expected: 42, got: nil (using ==) ./rpn_calculator_spec.rb:14: Finished in 0.016444 seconds 2 examples, 1 failureWhy not just create a method and in the same step get your Example passing? In this trivial example you could and it would not be too risky. However, in general you should make sure your Examples fail for the right reason before getting them to pass. Why? It serves as a check that the Example you've written is doing what you think it is doing.
With proper IDE support, the intermediate failing step to the working step is seconds. Even with the environment I'm using, two terminal windows, the work is 10 - 15 seconds. So while this is "extra" work, it has a purpose.
Eventually you will see these tutorials skipping steps. But for now, the tutorial is keeping the rigor high.
What about the hard-coded return value? Why not just write the code now, it's clear what needs to be done. This is an issue of balancing what you know and the risk that what you know is incorrect.
In this case, you could probably guess where this is going. However, as I write the tutorial, this is literally the first time I've worked on this part of this problem, so while I think I know what's going to happen, I'm really not sure. And even if I know the next step, I am not too sure about 2, 3 or more steps ahead.
A general answer to this complaint is that rather than writing what you know the solution should be, write an Example that will force/allow you to write the code you know should be there.
The primary danger of jumping ahead is writing too much production code. That code may not be well covered by your Examples, so you'll essentially have unvalidated code. In addition, you could write more than is necessary. If you do that, the next person to read the code might wonder why there's more code than the necessary. You might be that next person, by the way.
Check-In
Refactor
Review of the RpnCalculator class does not suggest any refactoring.
Review of the Examples, however, does. There is some minor duplication. Both Examples create a calculator. This is a quick fix since you can create a containing Context with common setup. As with any refactoring, however, you'll take small steps and verify as you go along.
Above the first describe
After the last end
endUpdate the spacing - that's one command in VI, the best-ever editor
You might be tempted to do more at this point. For example, you know you'll be adding a lot of values to the x_register. However, let that generalization happen naturally. Practicing both BDD and TDD encourages bottom-up, example-based generalization.
Check-In
Summary
The first example added basic object creation. This Example defined some of the API for entering digits. The production code doesn't actually process this yet, however, you already have executable documentation of how the API is supposed to work.
You also performed some basic refactoring on your Examples. While mentioned in the first tutorial, here are a few observations:
Example: Taking a decimal point as well
The production code is hard-coded. Somehow you want to push your solution to remove that hard-coded value. One way to do that is to and another Example (don't do this):On the one hand, this will force some more work in the production code. On the other hand, this Example is a proper superset of the first Example. That is, this Example completely covers the first Example, so, pragmatically, you should remove the first Example. More Examples does not necessarily mean better Examples. Each Example should push the solution a little further and avoid duplicating the work of another Example. Why?
This is not idle speculation, I've been on projects where this dynamic occurred. One example involved a situation where "certain tests" (this is TDD, not BDD) just failed and people tended to not worry about them. A developer added a new key-value pair to a hash table. This caused a test to fail. He figured that the test was failing for some other reason, so he ignored it.
In fact here's what was happening:
If tests fail for the wrong reasons, it makes it easy for someone to assume that what they just did could not possibly break the test. Often that just means an assumption is about to be exposed for what it is.
The DRY principle also covers test coverage.
So what can you do? How about accepting a decimal point in the number?
Example
Here is another example that fits extends the functionality just a little but will also force the implementation of the production code:
... <snip> ... - should handle a string of digits with an embedded . (FAILED - 1) 1) 'RPN Calculator Handling User Input of Numbers should handle a string of digits with an embedded .' FAILED expected: 13.31, got: 42 (using ==) ./rpn_calculator_spec.rb:27: Finished in 0.018738 seconds 3 examples, 1 failureNow it is time to do something more than hard-code the response.
- Run your Examples, you should be all green:
Check-InAnd as a reminder, the reason there are so many insertions in this example is because my checkins include updates to the source file used to generate the page you are reading.
Refactor
Reviewing the RpnCalclator class, there's little to do right now. What about your Examples? There appears to be duplication in that there are several lines that call the digit_pressed method. Is this duplication? It does represent the underlying objects' API. Even so, how can you improve this?
Before making any improvements, you need a reason to make the improvement. It is to make the Example more readable, easier to write, easier to maintain, or some other characteristic?
How about allowing your Example to provide a number, which is then "typed" for you?
Check-In
Summary
Congratulations, you have basic support for processing numeric input. You managed to use an Example that pushed the design just a bit and ended up with code in each of the methods.
There is one strange thing, your code uses "digit_pressed" for the digits 0 through 9 and '.'. That's something worth considering.
Here is a snapshot of what your code should resemble at this point:
rpn_calculator_spec.rb
rpn_calculator.rb
Example: What Happens with Enter?
What happens when a user presses the <enter> key on the calculator:In fact, as you will see, pressing a non-numeric key generally "locks-down" the x_register and makes the next digit cause it to be reset. You can describe this we several Examples.
Example
Here is a Context with three as yet to be defined Examples:
Before you can write any of these, you have to make a decision about how to indicate that the <enter> key was pressed. Here are a few options:
Is there a clear winner? There are trade-offs:
The Open/Closed Principle suggests that a class, once released into the wild, should not change again except for fixing errors. In Eiffel, this meant leaving a class alone and extending from it to add new behavior. In general, this can also mean depending on an abstraction (interface or abstract base class) and then adding subclasses to complete the behavior.
Why is this relevant? Gerald M. Weinberg describes a relationship between the size of a bug fix and the likelihood that the bug fix introduces new bugs. In his experience, the smaller the fix, the more likely a new bug will be introduced. In his research, 66% of all one-line bug fixes introduce new bugs.
So leaving working code alone is at the heart of the Open/Closed Principle.
Adding new methods to an existing class is in fact changing that class. Adding new methods to a class is less likely to cause problems, but it is not impossible break a class when you do add methods to it. This suggests adding methods to a class every time we add a new feature is probably a bad idea.
Another issue is, in general, the more methods on a class, the more likely it is that it will be hard to understand. Finally, having many methods on a class makes it more likely that the class will violate the Single Responsibility Principle.
This tutorial will take the second approach.
Why stop here? This Example is now failing because it invokes a method that does not yet exist.
This both completes the Example and causes it not to work because the top method does not exist. You can verify this by running your Examples.
Did you notice I just had you "cheat". You should have:
- Created a method that would cause the Example to fail.
- Run your Examples.
- Updated the method to return 654
- Run your Examples.
Did you notice that? If not, see how easy it is to slide backwards?If I have a proper IDE and decent refactoring tools, then I really do prefer following the three laws strictly. Unfortunately, I have not as of 2008/10 found an IDE that has two particular refactorings/corrections for Ruby:
I have these in C++, C#, Java, VB.Net and others. Since I don't have these, I'd consider relaxing the second rule of TDD to allow me to write an entire Example and then get add the missing Class/methods. I am not taking this approach in the tutorial because it doesn't add that much time and I'd rather you see how it is supposed to be practiced.
Is this realistic? Yes. Many developers better than I have practiced the three laws. In the simulator mentioned above, during the week I was there, we strictly followed TDD. I was not always at the keyboard but the work was being projected on an 1080i overhead projector so that kept the developers "honest."
Check-In
You have all passing Examples, but two are not implemented. Should you check in your work?
Refactor
A quick review of RpnCalculator doesn't suggest a need for refactoring. For now, the same can be said of the Examples.
Check-In
No need, there was no need to refactor.
Summary
Do you think pressing <enter> on a calculator is a function? Before I started this project my reaction would have been, "That's silly." However, when I got to this Example, I considered the writeup on the RPN Calculator and realized that the <enter> key is a function just as much as sqrt, pow, etc.
Have we made progress? Yes, we have extended the API of the calculator. What about checking in code with pending Examples, is that a good idea? I think it is OK, but it could cause problems. I prefer clean Example execution, so having several pending Examples is a bother. On the other hand, if I'm pairing with someone and one of us thinks of something worthy of validation, but it is not where we are working, we could write it down, email it, yell it to another team member, or we can put it in the code in an executable fashion.
Example: Checking that the x_register is reset-able
The next Example involves what happens after the <enter> key. You could have included this check in the previous Example, however keeping the tests small, focused and checking fewer things makes pinpointing problems easier.Example
Here's an example that seems to express the idea:
1) 'RPN Calculator Handling the <enter> key should cause next digit to reset the x_register' FAILED expected: 1, got: 6541.0 (using ==) ./rpn_calculator_spec.rb:41: Finished in 0.023285 seconds 6 examples, 1 failure, 1 pendingNotice that the 1 entered was appended to the x_register. So looks like this test pushes our current production code. You might also have observed that the support method named "enter_number" is really a bit off. Since <enter> has a meaning now, this method's name should be changed. That, however, is a refactoring so you should wait until you have all Examples passing (it OK to leave the pending Example pending).
def digit_pressed(digit) @input = '' if @x_register_should_reset x_register_should_reset = false @input << digit.to_s end def execute_function(function_symbol) @x_register_should_reset = true endCheck-In
Refactor
Here's a list of things that you might consider for refactoring:
enter_number
You can either change its name or make it do what it says. Flip a coin, you probably do not have enough information to make that decision just yet. However, in the spirit of keeping things clean, I did make a decision. I kept the method name and changed its implementation:
Violation of DRY between digit_pressed and initialize
This is one of those cases where duplication might be OK because right now initialize is simple but it is likely to get more complex later. I was originally going to change the implementation to this (don't do this):
However, before I made that change, it occurred to me that coupling to the initialize method will probably cause problems later. Even though I'd catch that when Executing my Examples, I instead decided to introduce a simple method that documents the intent.
Duplication in last two Examples
When you changed the implementation of enter_number to actually perform an <enter>, you could have updated the last two Examples as follows:
When these Examples reflect the update to the enter_number method, the duplication does not see to be bad.
Check-In
Summary
The behavior of the calculator has grown. The it also is beginning to handle the <enter> functionality. You removed duplication in the production code. It was small, and trivial and probably seemed like nothing. However, here's a principle from Jerry Weinberg:
If you are not maintaining your code and constantly cleaning it up, then it is decaying. Even if your code is not changing, it is decaying because the market is changing, the users' understanding of your system is changing, something is changing.
Keeping your code clean is not an event, it's not a processes, it is an attitude. You have it or you do not have it.
Are you going to be perfect?
No, you are going to miss stuff. That's why when you do find something, now is the time to handle it. If you cannot deal with it now, write it down on a piece of paper. Throw that piece of paper away in 2 days. If you haven't found the time to fix it by then, then by keeping the paper, you're just giving yourself undue stress.
Example: Should allow x_register to be entered again
This one has been pending. While I was just about to get to that, a few ideas went through my head:Those two items are officially on the punch-list. For now, let's get back to all green. We've been away from all green for too long.
Example
Well everything is all green. So this Example might not be adding value. Rather than immediately delete it, keep a note to review it after completing the first two items on the punch-list.
Check-In
Should you check in? You might not be sure about the Example, but you've got that to review after just a few more Examples. Even so, go ahead and commit. If you remove the Example later, your RCS tool can handle the workload.
Refactor
There are some duplicated validation steps in the Examples. Before refactoring, however, keep things as is until you've done just a little more work.
Check-In
There's nothing to check-in right now.
Summary
You've added an Example. You are not sure if it is a good one or not. You've got two items on your punch list. Review this Example after you have addressed those other two items.
Example: Resetting after the first digit
Once an example sends the execute_function method, the RpnCalculator will continue to reset. You need to fix this.Example
- should only reset after the first digit and not subsequent digits (FAILED - 1) 1) 'RPN Calculator Handling the <enter> key should only reset after the first digit and not subsequent digits' FAILED expected: 27, got: 7.0 (using ==) ./rpn_calculator_spec.rb:53: Finished in 0.023869 seconds 7 examples, 1 failureThe problem is, the "reset state" is set in execute_function but never reset.
There are two ways to fix this:
Update the digit_pressed method
Or Update the reset_input method
Either will work. In fact, doing both works. Question, to which method does the responsibility seem to bind? If you tought reset_input, you're right.
When I wrote "the solution" to the "should cause next digit to reset the x_register" Example, I put this logic in my code. I then realized that it was doing nothing to support any Examples so I removed it.
I then decided to create a test, the one you just worked on, to justify the need to reset the variable. If I had been working with a pairing partner, I'd hope my co-pilot would have called me on violating the third rule of TDD/BDD.
Check-In
Refactor
Reviewing the RpnCalculator there does not seem to be a need to do any refactoring. I do observe that the top method is still hard-coded, so an Example to drive that would be good.
As for the Examples, there is one thing you can do to clean up a little duplication. Every Example enters the number 654, so add a before method:
Check-In
With the one refactoring, now is a good time to check in your changes
Summary
You've pushed the solution a bit further and cleaned up some duplication. Specifically, resetting the value in the x_register seems to work and you've put some common setup work into a before for your Context.
You still have the question of whether there should or should not be a stack. And what about that pesky Example? You're keeping a punch-list, right?
Example: Enter actually puts values on the stack
Up to this point, your examples have addressed the effect of the <enter> key on the x_register, but what about stored values? Consider the following:What can you say about the RpnCalculator?
You already have validated the first two bullets with previous Examples, so there is no value in re-checking those features. You could add validation to existing examples to check for the second two values. I would advise against doing so because smaller tests tend to pinpoint problems better. Also, depending on the technology you are using, once a validation fails, subsequent validations are not checked in in RSpec, nor are the in most testing tools (FitNesse being one counter example).
Why is this last part important? When you've broken something, how big is the break? If your Examples fail, but each has one or a very small number of validations, you'll have a good idea of the span of the break. However, if you have Examples with dozens of validations (or many more), it is possible that an early validation will fail and you simply do not know if other validations would have failed. So while you think you only broke one thing, you broke 30 things. With many small tests, you'll have much higher fidelity information from which to move forward.
So with that in mind, you can continue with one a few more Examples working with <enter> to move towards validating the last two bullets.
Example
Here is an example that moves your production code just a little forward:
This Example suggests that after the first <enter> you should have two operands available, the one on the stack and the x_register.
Check-In
Refactor
There was little added to either of your two files, so there's not much to refactor right now. However, did you notice that you now have two hard-coded methods? This might suggest a direction in which you want to push your Examples.
Check-In
No need, no change.
Summary
This Example really just extended the API of the RpnCalcualtor. You might be feeling like it is time to get busy because of the two pending methods.
Example: Enter should increase the number of operands
The operator_count is hard-coded, this Example will make it harder to get away with that.Example
- should increase the operand count after each <enter> (FAILED - 1) 1) 'RPN Calculator Handling the <enter> key should increase the operand count after each <enter>' FAILED expected: 3, got: 2 (using ==) ./rpn_calculator_spec.rb:62: Finished in 0.026028 seconds 9 examples, 1 failureThere are two ways to make this work. Here's one way to get this to pass (don't do this):
Update initialize
Update execute_function
Update operand_count
Alternatively, you can use an array and actually store the values entered (notice, you change the same methods, add the same amount of code):
Update initialize
Update execute_function
Update operand_count
Which of these is the least amount of code that will get the Example to pass? Both involve 3 lines of code added to 3 methods. One uses an integer, one uses an array, and an array is more complex than an integer, though in Ruby they are both objects.
Why do you try to do the least amount possible to get an Example to pass? That guideline is a "how" not a "what or a why". That is, it is a means to an end. What end? There are at least a few:
In this case, the Example will exercise all of the added code in both approaches. What about over-design? When are you over designing? Are you over designing when you are using a design pattern where just a simple direct relationship is adequate? Yes. Are you over-designing when you a complex object structure, when a small, flat structure will suffice? Yes. Is over designing always a problem? NO. The problem is not over design. The problem is that over design leads to more rework when you've guess wrong. And as a developer, I can say that I guess wrong often.
Why do developers guess wrong? Because we generally see problems differently that people who provide requirements. Here's an analogy. On the Nintendo WII, there's a feature called "Mii's". A Mii is an avatar. I only created one because I had to for some games. I created one and never change it. My wife and daughter both have crated more than one, they update their looks and I'm convinced if there was a game that simply let players updated their Avatars with clothes, and makeup, etc, they'd buy it. If you asked me, it's a waste of time. But that is one of the may appeals to the system.
Problems change in ways that we often cannot anticipate simply because they are coming from a very different place. So when you write more code than is necessary at the moment, you're risking having to change code, or maybe make something that is designed for unnecessary flexibility, which could make the code harder to adapt to actual changes on the ground.
In this example, the domain is somewhat technical. Also, you can read the manual for RPN calculators and they will talk about a stack. So using an array as this code does is not diverging from the problem domain.
The second solution is a good one and it is not over design given the domain context.
Check-In
Refactor
A quick review of the code shows a hard-coded top method. Now that you have a stack in place, can you simply return the "top" of the array?
def top @operands.last endThis works. You have changed the production code without breaking any tests, so this is a legitimate refactoring.
Check-In
Summary
You have moved towards a more complete solution by introducing an array to hold operands. Was this over design? No. You certainly could have used the integer and then the stack, but that seems somewhat dogmatic.
You also managed to get rid of the last hard-coded method, top. This one Example managed to move the production code forward quite a bit.
Example: The right thing is on the stack
Right now, there's no way to know if what is on the stack is the right thing on the stack. Most of the pieces are in place, in fact even though the code is fully covered by the Examples, the Examples do not reflect fully what your production code can do. It is time to exploit that.Example
Here is an example that verifies the stack behavior of the RpnCalculator:
First observation: This code will not run because there is no pop! method on the RpnCalcualtor.
Check-In
Refactor
There has been a slowly-growing code small, Feature Envy, but hold off until just a bit longer. The "describe" part of the last Example is a huge clue.
Check-In
No need, you did not do any refactoring.
Summary
You have added an example to verify that in fact the right values are getting put onto the stack. Your RpnCalculator is growing, but at what point is the RpnCalcualtor suffering from too much design debt? It is right now, but you'll see that after the next Example.
Example: What happens with an 'Empty' stack?
The stack on an RpnCalculator is conceptually filled with 0's when it has no values. In fact, it is literally filed with 0's. So if you check the top of your RpnCalculator, calculator, it should return 0 when it is empty. The same is true for pop!. It should return 0 if the stack is empty.Example
These two are closely related, so here, for the first time, are two complete examples that you will fix at the same time.
- should return 0 when pop!'ing from an empty stack (FAILED - 1) - should return 0 for top when stack empty (FAILED - 2) 1) 'RPN Calculator Stack should return 0 when pop!'ing from an empty stack' FAILED expected: 0, got: nil (using ==) ./rpn_calculator_spec.rb:77: 2) 'RPN Calculator Stack should return 0 for top when stack empty' FAILED expected: 0, got: nil (using ==) ./rpn_calculator_spec.rb:81: Finished in 0.031744 seconds 12 examples, 2 failuresCheck-In
Check in, it is finally time to get busy removing Feature Envy and get closer to conforming to the Single Responsibility Principle.
Refactor
Review the following methods: pop!, top, available_operators. What do they all have in common? All three of these methods deal exclusively with the @operands instance variable.
Is this bad? Yes, it suggests that the RpnCalculator is doing work that should be pushed into the array. What? That's right, the array is not pulling its weight. This is a legitimate case for wrapping a collection class and giving it domain-specific behavior (semantics).
There is a name for methods in classes working exclusively with data in other objects. It is a code smell called Feature Envy.
Normally, at this point I'd say "you cannot change a system-defined class, so you either need to subclass or wrap and adapt". That is, create a subclass of Array, change the top method and add the pop method. Or, create a new class called something like OperandStack that holds a single instance of an array.
However, in Ruby, classes are never closed. You can add methods to a class anytime. Adding methods to Array is a bad idea since the Array class is used all over the place. However, Ruby also allows you to add methods to an instance.
This is crazy. Here is an example of how you could do that (don't do this):
The result from running this will be "true" printed twice. But if you try to simply create an array, you will not see the same results. Neither method is defined on the Array class.
Barring extending a single instance, it is certainly reasonable to create an OperandStack class, move these methods into it and then change the implementation of the RpnCalcualtor to use the OperandStack.
Before you start, did you notice that the Context for the last three Examples was "Stack"? That's a big clue that there's a missing class.
Next, you are going to do some "big" changes. It won't take long, but this will be the longest amount of time the tutorial will have you work before you can see your Examples running. Note your reaction to the change.
If you ran your examples now, the ones in the Stack context will fail. Why? Because they no longer pick up the automatically-created calculator object form the before in the "RPN Calculator" context.
This still won't pass. There's no OperandStack class.
Note that these are mostly just copies of methods from RpnCalculator. There is one new method, <<, which just delegates to a method of the same name.
Did you fully test this class? No. What you instead did was extract a class and then copy the tests that were directly testing it. You are not writing new code, you are refactoring the solution.
Intra-refactoring Check-In
Back to refactoring
Now you should refactor the RpnCalcualtor to use your newly-created class. As with other refactorings, you'll add, duplicate, validate and then remove code.
Check-In
Whew! That was a big refactoring. Check your code in!
Summary
This was a big change. You had a violation of feature envy. The RpnCalculator was doing a lot of work with information stored in an Array. So you created a class to represent that work, OperandStack, and moved Examples around to get everything situated.
If you have a decent IDE, these kinds of refactorings are quick, easy and fairly reliable. If not, then like Robert Martin tweeted:
This cleaned up your RpnCalculator class but there's still more to be done. The implementation of execute_function is wanting to do more (or less).
Is there a problem with how you are testing OpernadStack? There are only 2 Examples but quite a few methods for which you do not have unit tests. Those methods are in fact used from the RpnCalculator so you have coverage. However, if the OperandStack is used by other classes, then the testing is not adequate. Maybe you should consider a nested class. Maybe you should write more Examples to get its "contract" fully specified.
When you are just learning TDD or BDD, this kind of stuff is going to happen. Let's leave it alone for now and see what happens.
Do it!
I was tempted to just leave it at that, but I'll say a bit more. In my experience, most (anything beyond a trivial problem and most trivial problems) of the time I use a collection, at some point I'll want more behavior that the collection offers. The collection implements a "raw" zero-to-many relationship. Most of the time, my requirements want more than just a "raw" relationship. In the case of the RpnCalculator, it's stack is of infinite size and it always has zero after anything else that is pushed onto it. We simulated this by returning 0 if the stack is in fact empty.
It is nearly always a good idea to augment a system-defined collection with your own flair.
When you do, you have several options:
The option that always works, though it might require a touch more work, is the second option and what you did in this tutorial.
If you every have a collection within another collection, it's immediately time to follow this recommendation.
The Classes So Far
Here's one example of what all of your code should look like about now.
rpn_calculator_spec.rb
rpn_calculator.rb
operand_stack_spec.rb
operand_stack.rb
Example: A Binary Operator
Some time ago you created your first Example to exercise the <enter> key. That resulted in the following method:Now it is time start executing more functions. The first function you'll support is +.
Example
Here is a basic example to get started:
describe "Basic Math Operators" do it "should add the x_regiser and the top of the stack" do enter_number 46 @calculator.execute_function(:+) @calculator.x_register.should == 92 end endWhy is the result 92? The support method enter_number
1) 'RPN Calculator Basic Math Operators should add the x_regiser and the top of the stack' FAILED expected: 700, got: 46.0 (using ==) ./rpn_calculator_spec.rb:79: Finished in 0.03118 seconds 13 examples, 1 failureCheck-In
Check in your code. You are about to refactor.
Refactor
OK, the method you just updated, execute_function, is a bit of a mess. It does two thigns:
So you'll separate this for now.
Check-In
This is all the refactoring you'll do right now. More is on the horizaon.
Summary
You've improved the execute_function but you have a stubbed-out + method. If you know a bit of Ruby, you might be chomping at the bits to evaluate a symbol, or use lambda and a hash. The code will get where it will, however, based on the Examples, rather than speculation.
Example: Work on +
What should happen with + when there are no operands? Should it fail? Should it result in 0? If you're simulating an HP calculator, the result is 0. However, what it should do will be defined by the Examples you write.Example
Check-In
Check in your work.
Refactor
Something is starting to look strange. You may have noticed it the first time your code "supported" +. The following line:
I'm not sure what to do with that yet because there's not enough code to make many decisions. It might be that the code will stay put. It just seems a bit strange.
Check-In
No refactoring done, no need to check in again.
Summary
The + method no does work. It has one strange line, and there's still the issue of whether it modifies the stack correctly or not. Another thing occurred to me as well. After any operator, the x_register should be in "reset" mode.
Example: x_register in reset mode after +
Like <enter>, after perform +, the next character typed should clear the x_register and write into it. That seems like a good thing to check next.Example
1) 'RPN Calculator Basic Math Operators should reset the x_register after +' FAILED expected: 8, got: 9.08 (using ==) ./rpn_calculator_spec.rb:91: Finished in 0.032479 seconds 16 examples, 1 failureCheck-In
Check in your code. This time you will do a little bit of refactorig.
Refactor
There is a duplicated line in the enter and + methods:
At the very least, this line of code could be put into a well-named method.
Check-In
Even though small, you did make a change to your solution while keeping your Examples passing, so check in your work.
Summary
There appears to be a pattern emerging. Two functions, enter and +, both of which change the calculator into "override_mode". Will other functions look like this? Yes. Will you be able to remove this duplication as well? Yes.
Example: What about the stack?
Some time back the tutorial asked about whether the stack was treated properly. You could have put those checks into another Example, but the rationale for not doing that is to keep your tests small and focused for better pinpointing of problems and better assessment of the size of a break.It's time to come back to that, if for no other reason than to expression a "contract" that the + method (and all binary operators) will need to follow.
Example
Here's an example:
Did we code too much or are we validating a behavior? Would it have been possible to write less code and still keep other tests passing? Maybe, the design is such that it's getting harder to do the wrong thing. In any case, this seems like an OK Example - if maybe a bit off target.
Check-In
Check in your work.
Refactor
There's starting to be a bit of cruft in the Examples and maybe something in the RpnCalcualtor. You might already be noticing yourself. For example, in RpnCalculator, to set the "x_register" the code does the following:
That is not self evident. In addition, getting two operands for a binary operator involves two very different expressions:
The last two Examples are OK, though do you believe that the differences between digit_pressed and enter_number is obvious?
Start by making an improvement to the bottom part of the + method:
The next thing you might consider is how to better document getting parameters for +. I tried a few ways but none seemed to really make the code better. Just differently convoluted. You might try yourself (and even succeed). However, I already have an idea of where I want this code to go, so rather than any more refactoring, I want to move into other operators.
Check-In
Summary
You now have the basics of the + operator worked out. You make resetting the x_register at least a little better. Now it is time to add some more operators to see how this current solution grows.
Example: Support for a new binary operator, -
Adding support for - will allow you to see how your solution grows as new math functions are supported. You have already vetted + fairly well, so adding - should be a snap.Example
At this point, hopefully you are getting bothered by the difference between lines that start with an @ and ones that do not. Add that to your punch-list.
Were you surprised by the result? The results suggest that instead of -, your calculator performed +. Why is that?
A quick review of execute_function should give you a clue:
So if something is not :enter, then it is :+. Add that to your punch-list:
You have two obvious choices here:
Check-In
Check in, you have a lot on your punch-list.
Refactor
There are several things on your punch-list:
Looks like you have a lot of work.
First, if you have been chomping at the bits to fix execute_function, now is the time to do so:
This is a Ruby idiom. The original approach of passing in a symbol screamed for this solution. This has several effects
Even so, this is an improvement and it follows a Ruby idiom.
Next, you'll remove the violation of the DRY principle:
Looks like you've managed to fix the first three things on your punch-list with a few quick changes in your code. Of course, you still do not have an Example that captures what happens when you execute a bogus method, the code should handle that now.
Before calling this refactoring session done, clean up your Examples to make this a bit more self-explanatory.
Which do you prefer? You have seen three different forms:
The first form gives a concrete example of how the RpnCalculator should be used. The last form makes the Example easy to read and moves the writing of the examples into the way a user might speak about using the calculator.
Here is just he Basic Math Operators context updated:
Here's a side-by-side comparison between the two styles:
it "programmer" do
- @calculator.digit_pressed 4
- @calculator.execute_function :enter
- @calculator.digit_pressed 5
- @calculator.digit_pressed 2
- @calculator.execute_function :+
- @calculator.x_register.should == 56
endit "user" do- press :+
- validate_x_register 56
endBefore thinking that one style is the "right" style, consider the audience and even the author. If the audience is the user, then the style on the right is more appropriate. If the audience is another developer, then the left side might be more appropriate.
You might think that using the methods names on the right for your calculator is an option. However, that won't look correct:
The method names do not make sense being sent to a calculator. So the method names make sense on the left when sent to a calculator. The method names on the right make sense when reading an example.
While this tutorial will not cover it, RSpec has the notion of "story tests", or tests that are meant to be written by the same people who write user stories, your customer, product owner, QA person or even developers. That's coming up in a later tutorial, so this subject will come up again.
However, just to place a (somewhat arbitrary) stake in the ground, here's an updated version of rpn_calculator_spec.rb:
Check-In
Summary
That was a lot of refactoring. Maybe the tutorial let you collect too much design debt. However, this will happen and it is up to you to pick up the slack.
You made several important changes:
As a result of this change, your execute_function will indicate errors better than when you added an Example for - (which caused + to get called)
As a result, you removed some DRY violations.
What's left? There's still the issue of documenting what happens when you attempt to execute a missing function and the rpn_calculator_spec.rb file is getting large.
This second issue is not addressed in this tutorial.
Example: Handling missing functions
So what happens when your calculator is asked to execute an unknown function?Example
This Example documents that you expect the behavior of throwing a NoMethodError when sending a bogus method name to your calculator.
Check-In
Refactor
You just did a bunch of refactoring. You only added an Example to capture something that was added during refactoring but not explicitly spelled out in any Example. So nothing new to refactor.
Check-In
You have no changes to check in.
Summary
This example really just captured something about your system that was not explicitly spelled out anywhere. This probably indicates too much change done during refactoring. It would have been possible for you to wait to change the implementation of execute_function and first write this Example.
You won't always figure out to do that, so you've managed to recover gracefully.
Example: Add support for * and /
You have a working design for + and -, adding * and / should be a snap. First your calculator will support multiplication.Example
That was so quick, before checking in add one more example:
Check-In
Check in your work.
Refactor
You might review your code and find some things to refactor. One issue is the size of the calculator. Right now it is not too bad, but it's going to get bigger. Each time you add a function, it grows.
Check-In
Nothing new to check in.
Summary
You've just finished out the basics for your calculator.
RUNNING OUT, COPY AGAIN
Example x
OverviewExample
Check-In
Refactor
Check-In
Summary
Example x
OverviewExample
Check-In
Refactor
Check-In
Summary
Back